@@ -30,6 +30,10 @@ Licensed to the Apache Software Foundation (ASF) under one
3030#import < MobileCoreServices/UTCoreTypes.h>
3131#import < objc/message.h>
3232#import < Photos/Photos.h>
33+ // Since iOS 14, we can use PHPickerViewController to select images from the photo library
34+ #if __has_include(<PhotosUI/PhotosUI.h>)
35+ #import < PhotosUI/PhotosUI.h>
36+ #endif
3337
3438#ifndef __CORDOVA_4_0_0
3539 #import < Cordova/NSData+Base64.h>
@@ -184,26 +188,40 @@ - (void)takePicture:(CDVInvokedUrlCommand*)command
184188 }
185189 }];
186190 } else {
187- [weakSelf options: pictureOptions requestPhotoPermissions: ^(BOOL granted) {
188- if (!granted) {
189- // Denied; show an alert
190- dispatch_async (dispatch_get_main_queue (), ^{
191- UIAlertController *alertController = [UIAlertController alertControllerWithTitle: [[NSBundle mainBundle ] objectForInfoDictionaryKey: @" CFBundleDisplayName" ] message: NSLocalizedString(@" Access to the camera roll has been prohibited; please enable it in the Settings to continue." , nil ) preferredStyle: UIAlertControllerStyleAlert];
192- [alertController addAction: [UIAlertAction actionWithTitle: NSLocalizedString(@" OK" , nil ) style: UIAlertActionStyleDefault handler: ^(UIAlertAction * _Nonnull action) {
193- [weakSelf sendNoPermissionResult: command.callbackId];
194- }]];
195- [alertController addAction: [UIAlertAction actionWithTitle: NSLocalizedString(@" Settings" , nil ) style: UIAlertActionStyleDefault handler: ^(UIAlertAction * _Nonnull action) {
196- [[UIApplication sharedApplication ] openURL: [NSURL URLWithString: UIApplicationOpenSettingsURLString]];
197- [weakSelf sendNoPermissionResult: command.callbackId];
198- }]];
199- [weakSelf.viewController presentViewController: alertController animated: YES completion: nil ];
200- });
201- } else {
202- dispatch_async (dispatch_get_main_queue (), ^{
203- [weakSelf showCameraPicker: command.callbackId withOptions: pictureOptions];
204- });
205- }
206- }];
191+ // For photo library on iOS 14+, PHPickerViewController doesn't require permissions
192+ // Only request permissions if we're on iOS < 14 or need UIImagePickerController
193+ BOOL needsPermissionCheck = YES ;
194+ if (@available (iOS 14 , *)) {
195+ needsPermissionCheck = NO ; // PHPickerViewController will be used, no permission needed
196+ }
197+
198+ if (needsPermissionCheck) {
199+ [weakSelf options: pictureOptions requestPhotoPermissions: ^(BOOL granted) {
200+ if (!granted) {
201+ // Denied; show an alert
202+ dispatch_async (dispatch_get_main_queue (), ^{
203+ UIAlertController *alertController = [UIAlertController alertControllerWithTitle: [[NSBundle mainBundle ] objectForInfoDictionaryKey: @" CFBundleDisplayName" ] message: NSLocalizedString(@" Access to the camera roll has been prohibited; please enable it in the Settings to continue." , nil ) preferredStyle: UIAlertControllerStyleAlert];
204+ [alertController addAction: [UIAlertAction actionWithTitle: NSLocalizedString(@" OK" , nil ) style: UIAlertActionStyleDefault handler: ^(UIAlertAction * _Nonnull action) {
205+ [weakSelf sendNoPermissionResult: command.callbackId];
206+ }]];
207+ [alertController addAction: [UIAlertAction actionWithTitle: NSLocalizedString(@" Settings" , nil ) style: UIAlertActionStyleDefault handler: ^(UIAlertAction * _Nonnull action) {
208+ [[UIApplication sharedApplication ] openURL: [NSURL URLWithString: UIApplicationOpenSettingsURLString]];
209+ [weakSelf sendNoPermissionResult: command.callbackId];
210+ }]];
211+ [weakSelf.viewController presentViewController: alertController animated: YES completion: nil ];
212+ });
213+ } else {
214+ dispatch_async (dispatch_get_main_queue (), ^{
215+ [weakSelf showCameraPicker: command.callbackId withOptions: pictureOptions];
216+ });
217+ }
218+ }];
219+ } else {
220+ // iOS 14+ with PHPickerViewController - no permission needed, show picker directly
221+ dispatch_async (dispatch_get_main_queue (), ^{
222+ [weakSelf showCameraPicker: command.callbackId withOptions: pictureOptions];
223+ });
224+ }
207225 }
208226 }];
209227}
@@ -212,6 +230,15 @@ - (void)showCameraPicker:(NSString*)callbackId withOptions:(CDVPictureOptions *)
212230{
213231 // Perform UI operations on the main thread
214232 dispatch_async (dispatch_get_main_queue (), ^{
233+ // Use PHPickerViewController for photo library on iOS 14+
234+ #if __has_include(<PhotosUI/PhotosUI.h>)
235+ if (pictureOptions.sourceType == UIImagePickerControllerSourceTypePhotoLibrary) {
236+ [self showPHPicker: callbackId withOptions: pictureOptions];
237+ return ;
238+ }
239+ #endif
240+
241+ // Fallback to UIImagePickerController for camera or older iOS versions
215242 CDVCameraPicker* cameraPicker = [CDVCameraPicker createFromPictureOptions: pictureOptions];
216243 self.pickerController = cameraPicker;
217244
@@ -242,6 +269,40 @@ - (void)showCameraPicker:(NSString*)callbackId withOptions:(CDVPictureOptions *)
242269 });
243270}
244271
272+ // Since iOS 14, we can use PHPickerViewController to select images from the photo library
273+ #if __has_include(<PhotosUI/PhotosUI.h>)
274+ - (void )showPHPicker : (NSString *)callbackId withOptions : (CDVPictureOptions*)pictureOptions API_AVAILABLE(ios(14 ))
275+ {
276+ PHPickerConfiguration *config = [[PHPickerConfiguration alloc ] init ];
277+
278+ // Configure filter based on media type
279+ if (pictureOptions.mediaType == MediaTypeVideo) {
280+ config.filter = [PHPickerFilter videosFilter ];
281+ } else if (pictureOptions.mediaType == MediaTypeAll) {
282+ config.filter = [PHPickerFilter anyFilterMatchingSubfilters: @[
283+ [PHPickerFilter imagesFilter ],
284+ [PHPickerFilter videosFilter ]
285+ ]];
286+ } else {
287+ config.filter = [PHPickerFilter imagesFilter ];
288+ }
289+
290+ config.selectionLimit = 1 ;
291+ config.preferredAssetRepresentationMode = PHPickerConfigurationAssetRepresentationModeCurrent;
292+
293+ PHPickerViewController *picker = [[PHPickerViewController alloc ] initWithConfiguration: config];
294+ picker.delegate = self;
295+
296+ // Store callback ID and options
297+ objc_setAssociatedObject (picker, " callbackId" , callbackId, OBJC_ASSOCIATION_RETAIN_NONATOMIC );
298+ objc_setAssociatedObject (picker, " pictureOptions" , pictureOptions, OBJC_ASSOCIATION_RETAIN_NONATOMIC );
299+
300+ [self .viewController presentViewController: picker animated: YES completion: ^{
301+ self.hasPendingOperation = NO ;
302+ }];
303+ }
304+ #endif
305+
245306- (void )sendNoPermissionResult : (NSString *)callbackId
246307{
247308 CDVPluginResult* result = [CDVPluginResult resultWithStatus: CDVCommandStatus_ERROR messageAsString: @" has no access to camera" ]; // error callback expects string ATM
@@ -898,6 +959,154 @@ - (void)imagePickerControllerReturnImageResult
898959 }
899960}
900961
962+ #if __has_include(<PhotosUI/PhotosUI.h>)
963+ // PHPickerViewController Delegate Methods (iOS 14+)
964+ - (void )picker : (PHPickerViewController *)picker didFinishPicking : (NSArray <PHPickerResult *> *)results API_AVAILABLE(ios(14 ))
965+ {
966+ NSString *callbackId = objc_getAssociatedObject (picker, " callbackId" );
967+ CDVPictureOptions *pictureOptions = objc_getAssociatedObject (picker, " pictureOptions" );
968+
969+ __weak CDVCamera* weakSelf = self;
970+
971+ [picker dismissViewControllerAnimated: YES completion: ^{
972+ if (results.count == 0 ) {
973+ // User cancelled
974+ CDVPluginResult* result = [CDVPluginResult resultWithStatus: CDVCommandStatus_ERROR messageAsString: @" No Image Selected" ];
975+ [weakSelf.commandDelegate sendPluginResult: result callbackId: callbackId];
976+ weakSelf.hasPendingOperation = NO ;
977+ return ;
978+ }
979+
980+ PHPickerResult *pickerResult = results.firstObject ;
981+
982+ // Check if it's a video
983+ if ([pickerResult.itemProvider hasItemConformingToTypeIdentifier: UTTypeMovie.identifier]) {
984+ [pickerResult.itemProvider loadFileRepresentationForTypeIdentifier: UTTypeMovie.identifier completionHandler: ^(NSURL * _Nullable url, NSError * _Nullable error) {
985+ if (error) {
986+ CDVPluginResult* result = [CDVPluginResult resultWithStatus: CDVCommandStatus_ERROR messageAsString: [error localizedDescription ]];
987+ [weakSelf.commandDelegate sendPluginResult: result callbackId: callbackId];
988+ weakSelf.hasPendingOperation = NO ;
989+ return ;
990+ }
991+
992+ dispatch_async (dispatch_get_main_queue (), ^{
993+ NSString * videoPath = [weakSelf createTmpVideo: [url path ]];
994+ CDVPluginResult* result = [CDVPluginResult resultWithStatus: CDVCommandStatus_OK messageAsString: videoPath];
995+ [weakSelf.commandDelegate sendPluginResult: result callbackId: callbackId];
996+ weakSelf.hasPendingOperation = NO ;
997+ });
998+ }];
999+ }
1000+ // Handle image
1001+ else if ([pickerResult.itemProvider canLoadObjectOfClass: [UIImage class ]]) {
1002+ [pickerResult.itemProvider loadObjectOfClass: [UIImage class ] completionHandler: ^(__kindof id <NSItemProviderReading > _Nullable object, NSError * _Nullable error) {
1003+ if (error) {
1004+ CDVPluginResult* result = [CDVPluginResult resultWithStatus: CDVCommandStatus_ERROR messageAsString: [error localizedDescription ]];
1005+ [weakSelf.commandDelegate sendPluginResult: result callbackId: callbackId];
1006+ weakSelf.hasPendingOperation = NO ;
1007+ return ;
1008+ }
1009+
1010+ UIImage *image = (UIImage *)object;
1011+
1012+ // Get asset identifier to fetch metadata
1013+ NSString *assetIdentifier = pickerResult.assetIdentifier ;
1014+
1015+ dispatch_async (dispatch_get_main_queue (), ^{
1016+ [weakSelf processPHPickerImage: image assetIdentifier: assetIdentifier callbackId: callbackId options: pictureOptions];
1017+ });
1018+ }];
1019+ }
1020+ }];
1021+ }
1022+
1023+ - (void )processPHPickerImage : (UIImage*)image assetIdentifier : (NSString *)assetIdentifier callbackId : (NSString *)callbackId options : (CDVPictureOptions*)options API_AVAILABLE(ios(14 ))
1024+ {
1025+ __weak CDVCamera* weakSelf = self;
1026+
1027+ // Fetch metadata if asset identifier is available
1028+ if (assetIdentifier) {
1029+ PHFetchResult *result = [PHAsset fetchAssetsWithLocalIdentifiers: @[assetIdentifier] options: nil ];
1030+ PHAsset *asset = result.firstObject ;
1031+
1032+ if (asset) {
1033+ PHImageRequestOptions *imageOptions = [[PHImageRequestOptions alloc ] init ];
1034+ imageOptions.synchronous = YES ;
1035+ imageOptions.networkAccessAllowed = YES ;
1036+
1037+ [[PHImageManager defaultManager ] requestImageDataForAsset: asset options: imageOptions resultHandler: ^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) {
1038+ NSDictionary *metadata = nil ;
1039+ if (imageData) {
1040+ metadata = [weakSelf convertImageMetadata: imageData];
1041+ }
1042+
1043+ dispatch_async (dispatch_get_main_queue (), ^{
1044+ [weakSelf finalizePHPickerImage: image metadata: metadata callbackId: callbackId options: options];
1045+ });
1046+ }];
1047+ return ;
1048+ }
1049+ }
1050+
1051+ // No metadata available
1052+ [self finalizePHPickerImage: image metadata: nil callbackId: callbackId options: options];
1053+ }
1054+
1055+ - (void )finalizePHPickerImage : (UIImage*)image metadata : (NSDictionary *)metadata callbackId : (NSString *)callbackId options : (CDVPictureOptions*)options API_AVAILABLE(ios(14 ))
1056+ {
1057+ // Process image according to options
1058+ UIImage *processedImage = image;
1059+
1060+ if (options.correctOrientation ) {
1061+ processedImage = [processedImage imageCorrectedForCaptureOrientation ];
1062+ }
1063+
1064+ if ((options.targetSize .width > 0 ) && (options.targetSize .height > 0 )) {
1065+ if (options.cropToSize ) {
1066+ processedImage = [processedImage imageByScalingAndCroppingForSize: options.targetSize];
1067+ } else {
1068+ processedImage = [processedImage imageByScalingNotCroppingForSize: options.targetSize];
1069+ }
1070+ }
1071+
1072+ // Create info dictionary similar to UIImagePickerController
1073+ NSMutableDictionary *info = [NSMutableDictionary dictionary ];
1074+ [info setObject: processedImage forKey: UIImagePickerControllerOriginalImage];
1075+ if (metadata) {
1076+ [info setObject: metadata forKey: @" UIImagePickerControllerMediaMetadata" ];
1077+ }
1078+
1079+ // Store metadata for processing
1080+ if (metadata) {
1081+ self.metadata = [[NSMutableDictionary alloc ] init ];
1082+
1083+ NSMutableDictionary * EXIFDictionary = [[metadata objectForKey: (NSString *)kCGImagePropertyExifDictionary ] mutableCopy ];
1084+ if (EXIFDictionary) {
1085+ [self .metadata setObject: EXIFDictionary forKey: (NSString *)kCGImagePropertyExifDictionary ];
1086+ }
1087+
1088+ NSMutableDictionary * TIFFDictionary = [[metadata objectForKey: (NSString *)kCGImagePropertyTIFFDictionary ] mutableCopy ];
1089+ if (TIFFDictionary) {
1090+ [self .metadata setObject: TIFFDictionary forKey: (NSString *)kCGImagePropertyTIFFDictionary ];
1091+ }
1092+
1093+ NSMutableDictionary * GPSDictionary = [[metadata objectForKey: (NSString *)kCGImagePropertyGPSDictionary ] mutableCopy ];
1094+ if (GPSDictionary) {
1095+ [self .metadata setObject: GPSDictionary forKey: (NSString *)kCGImagePropertyGPSDictionary ];
1096+ }
1097+ }
1098+
1099+ __weak CDVCamera* weakSelf = self;
1100+
1101+ // Process and return result
1102+ [self resultForImage: options info: info completion: ^(CDVPluginResult* res) {
1103+ [weakSelf.commandDelegate sendPluginResult: res callbackId: callbackId];
1104+ weakSelf.hasPendingOperation = NO ;
1105+ weakSelf.pickerController = nil ;
1106+ }];
1107+ }
1108+ #endif
1109+
9011110@end
9021111
9031112@implementation CDVCameraPicker
0 commit comments