-
Notifications
You must be signed in to change notification settings - Fork 0
[iOS] 권한 설정 없이 사진의 시간 메타데이터 가져오기
앨범에 접근해서 사진을 선택할 수 있는 방식은
- UIImagePickerController
- PHPickerViewController ✅
두 가지가 있습니다.
이 중에서 저희는 두 번째 방식인 PHPicker을 선택했는데요.
PHPicker는 다음과 같은 특징을 가집니다.
- 검색, 다중 선택, 줌인 줌아웃 등 다양한 기능 제공
- 권한 설정 불필요 🚀
이 중 두 번째 특징을 이유로 선택하게 되었습니다.
단순히 한 장을 사진을 업로드하여 보여주는 것 이외의 기능이 필요하지 않았기 때문에 권한 설정이나 이로 인한 분기처리에 대한 리소스를 줄이고자 했습니다.
하지만 서비스 개발을 진행하면서 시간 데이터를 가져와 활용할 수 있다면 사용자가 더 편하게 사용할 수 있지 않을까?란 피드백을 받고, 적용하게 되었습니다.
그러면서 사진의 메타데이터에 대한 정보를 가져오기 위해선 권한설정이 필수지 않을까?란 생각을 하게 되었고 다시 한 번 PHPicker를 검토했는데요.
놀랍게도 PHPicker를 사용하면 메타데이터까지도 따로 권한을 물어보지 않아도 가져올 수 있습니다.
어떻게 가능할까요 ?
그 이유는 PHPicker 동작 방식에 있는데요.
기본적으로 비공개로 제공되며 사용자 사진 라이브러리에 직접 액세스하지 않습니다.
그래서 사진 라이브러리 액세스에 대한 요청을 보여주지 않게되며 결론적으로 사용자가 선택한 사진 및 비디오만 제공되죠.
좀 더 자세하게 살펴보자면,
PHPicker는 위의 그림과 같이 앱의 프로세스 외부에서 실행됩니다.
앱 내에서 실행되는 것처럼 보이지만 실제로는 앱의 위에 랜더링되는 별도의 프로세스에서 실행됩니다.
그렇기에 앱은 실제로 picker에 직접 접근할 수도 없고, picker 콘텐츠의 스크린샷조차 찍을 수 없습니다.
사용자가 실제로 선택한 사진만이 앱으로 전달될 뿐이죠.
그렇다면 이제 선택한 사진의 시간 메타데이터를 가져와봅시다.
메타데이터를 가져오기 위한 방법을 찾아보면 가장 많이 나오는 방식이 PHAsset을 함께 사용하는 방식입니다.
PHPicker로 선택한 사진을 기반으로 PHAsset을 사용하여 메타데이터들을 가져오는 것이죠.
하지만, 이를 위해선 사진에 대한 권한 설정이 필요합니다.
아까 언급했듯이 저희는 권한 설정을 하지 않아도 되는 PHPicker를 선택했기에 이 장점을 그대로 가져가고자 했습니다.
그럼 어떻게 가져올 수 있을까요?
PHPicker을 통해 받은 itemProvider의 메서드들을 통해 데이터를 받아올 수 있습니다.
[1] 먼저 선택된 사진이 원하는 이미지 유형 식별자를 준수하는지 확인하여 속성 목록을 검색합니다.
if imageResult.itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier)
[2] 존재한다면, 데이터를 로드하여 메타데이터로 변환하는 과정이 필요합니다.
imageResult.itemProvider.loadDataRepresentation(forTypeIdentifier: UTType.image.identifier) { [weak self] data, _ in
guard let data = data,
let cgImageSource = CGImageSourceCreateWithData(data as CFData, nil),
let metadata = CGImageSourceCopyPropertiesAtIndex(cgImageSource, 0, nil) as? [String: Any],
let exif = metadata[Constants.metaDataKey] as? [String: Any],
let date = exif[Constants.dateKey] as? String
else { return }
}
[2-1] 데이터를 CGImageSource를 통해 CGImage로 변환합니다.
[2-2] CGImageSourceCopyPropertiesAtIndex를 통해 CFDictionary를 받아오게 되는데 이를 [String: Any] 형식으로 변환합니다.
[2-3] 저희가 필요한 값은 시간 데이터기 때문에 Exif > DateTimeOriginal 값을 추출하기 위해 형 변환을 다시 거칩니다.
결론적으로 문자열 형태의 시간 데이터인 date를 받아올 수 있게 됩니다.
이 때, [2-2]에서 받아오는 CFDictionary에는 픽셀, 크기, TIFF, Exif 등의 정보가 담겨있습니다.
실제로 내부에 존재하는 데이터 예시입니다.
["{GPS}": {
Altitude = "35.88339426592265";
AltitudeRef = 0;
DateStamp = "2023:12:09";
DestBearing = "185.7189178436443";
DestBearingRef = T;
HPositioningError = "21.76776649746193";
ImgDirection = "185.7189178436443";
ImgDirectionRef = T;
Latitude = "37.51792166666667";
LatitudeRef = N;
Longitude = "126.9034716666667";
LongitudeRef = E;
Speed = 0;
SpeedRef = K;
TimeStamp = "13:02:54";
}, "{ExifAux}": {
Regions = {
HeightAppliedTo = 3024;
RegionList = (
{
AngleInfoRoll = 276;
AngleInfoYaw = 0;
ConfidenceLevel = 90;
FaceID = 41;
Height = "0.04234045689019894";
Type = Face;
Width = "0.03152238805970153";
X = "0.5755798783858486";
Y = "0.535286661753869";
}
);
WidthAppliedTo = 4032;
};
}, "Depth": 8, "PixelWidth": 4032, "DPIWidth": 72, "ProfileName": Display P3, "DPIHeight": 72, "{MakerApple}": {
1 = 14;
12 = (
"0.6328125",
"0.8515625"
);
13 = 0;
14 = 0;
16 = 1;
2 = {length = 512, bytes = 0x06000800 08000a00 12002f00 4a004000 ... 39005700 41001600 };
20 = 10;
22 = AcvCPWezEPoNmUUuGsjpT7DARu3S;
23 = 1112547328;
25 = 2;
26 = q825s;
3 = {
epoch = 0;
flags = 1;
timescale = 1000000000;
value = 229775551632333;
};
31 = 0;
32 = "3E54AF88-60B2-4AC4-A6E2-AE4732E02396";
33 = "0.5452924";
35 = (
119,
268435486
);
37 = 142;
38 = 3;
39 = "40.6595";
4 = 1;
40 = 2;
43 = "F245E99D-D243-4373-BADA-83D0BB59B8B6";
45 = 4420;
46 = 1;
47 = 38;
48 = "0.0136868";
5 = 173;
51 = 4096;
52 = 4;
53 = 3;
54 = 238;
55 = 4;
58 = 243;
59 = 0;
6 = 165;
60 = 4;
63 = 1;
64 = {
0 = 1;
1 = 0;
2 = 0;
3 = 0;
};
65 = 0;
67 = 0;
68 = 0;
69 = 0;
7 = 1;
70 = 0;
74 = 2;
77 = "35.80603";
78 = {
1 = 1;
2 = (
{
"2.1" = "60.06072998046875";
"2.2" = 65460;
},
{
"2.1" = 0;
"2.2" = 13;
}
);
};
79 = 0;
8 = (
"-0.002398995",
"-0.9739192",
"0.207662"
);
}, "{Exif}": {
ApertureValue = "1.356143809255609";
BrightnessValue = "3.199670972459176";
ColorSpace = 65535;
ComponentsConfiguration = (
1,
2,
3,
0
);
CompositeImage = 2;
DateTimeDigitized = "2023:12:09 22:02:55";
DateTimeOriginal = "2023:12:09 22:02:55";
DigitalZoomRatio = "1.114222549742078";
ExifVersion = (
2,
3,
2
);
ExposureBiasValue = 0;
ExposureMode = 0;
ExposureProgram = 2;
ExposureTime = "0.01666666666666667";
FNumber = "1.6";
Flash = 16;
FlashPixVersion = (
1,
0
);
FocalLenIn35mmFilm = 28;
FocalLength = "5.1";
ISOSpeedRatings = (
64
);
LensMake = Apple;
LensModel = "iPhone 13 mini back dual wide camera 5.1mm f/1.6";
LensSpecification = (
"1.54",
"5.1",
"1.6",
"2.4"
);
MeteringMode = 3;
OffsetTime = "+09:00";
OffsetTimeDigitized = "+09:00";
OffsetTimeOriginal = "+09:00";
PixelXDimension = 4032;
PixelYDimension = 3024;
SceneCaptureType = 0;
SceneType = 1;
SensingMethod = 2;
ShutterSpeedValue = "5.90642900670323";
SubjectArea = (
2037,
2642,
748,
754
);
SubsecTimeDigitized = 149;
SubsecTimeOriginal = 149;
WhiteBalance = 0;
}, "Orientation": 6, "PrimaryImage": 1, "ColorModel": RGB, "{TIFF}": {
DateTime = "2023:12:09 22:02:55";
HostComputer = "iPhone 13 mini";
Make = Apple;
Model = "iPhone 13 mini";
Orientation = 6;
ResolutionUnit = 2;
Software = "17.1.2";
XResolution = 72;
YResolution = 72;
}, "PixelHeight": 3024]