From 8b530233dd21eadb162f35416a98c7ea6ea6df39 Mon Sep 17 00:00:00 2001 From: Vijay Vikram Singh Date: Mon, 14 Sep 2020 07:19:20 -0700 Subject: [PATCH] feat(ios): allow multiple photo selection (#11867) * add workaround code for video loading * updated logic for selectionLimit to work with allowMultiple true only Fixes TIMOB-27984 --- apidoc/Titanium/Media/Media.yml | 40 ++++++- iphone/Classes/MediaModule.h | 10 +- iphone/Classes/MediaModule.m | 192 +++++++++++++++++++++++++++++++- 3 files changed, 236 insertions(+), 6 deletions(-) diff --git a/apidoc/Titanium/Media/Media.yml b/apidoc/Titanium/Media/Media.yml index ba77e7cac4f..8fd3001e142 100644 --- a/apidoc/Titanium/Media/Media.yml +++ b/apidoc/Titanium/Media/Media.yml @@ -2054,7 +2054,10 @@ summary: | properties: - name: success summary: Function to call when the photo gallery is closed after a successful selection. - type: Callback + description: | + If is `true`, then Callback will be invoked. + Otherwise Callback will be invoked for single selection. + type: [Callback, Callback] - name: error summary: Function to call upon receiving an error. type: Callback @@ -2104,8 +2107,18 @@ properties: description: | The allowMultiple property is only available on Android API 18 and above. type: Boolean - platforms: [android] - since: "6.0.0" + platforms: [android, iphone, ipad] + osver: {ios: {min: "14.0"}} + since: { android: "6.0.0", iphone: "9.2.0", ipad: "9.2.0" } + - name: selectionLimit + summary: Specifies number of media item that can be selected. + description: | + Setting this property to zero allows you to select maximum number of media supported by system. + This will work only when is `true`. + type: Boolean + platforms: [iphone, ipad] + osver: {ios: {min: "14.0"}} + since: "9.2.0" - name: allowTranscoding summary: Specifies if the video should be transcoded (using highest quality preset) . If set to false no video transcoding will be performed. type: Boolean @@ -2115,6 +2128,27 @@ properties: osver: {ios: {min: "11.0"}} --- +name: CameraMediaMultipleItemsType +summary: A media object from photo gallery when is `true`. +extends: SuccessResponse +properties: + - name: images + summary: | + The list of selected images. + type: Array + optional: true + - name: livePhotos + summary: | + The list of selected live photo objects. + type: Array + platforms: [iphone, ipad] + optional: true + - name: videos + summary: | + The list of selected videos. + type: Array + optional: true +--- name: CameraMediaItemType summary: A media object from the camera or photo gallery. extends: SuccessResponse diff --git a/iphone/Classes/MediaModule.h b/iphone/Classes/MediaModule.h index 5e0a416089c..c0248a4a41f 100644 --- a/iphone/Classes/MediaModule.h +++ b/iphone/Classes/MediaModule.h @@ -9,6 +9,9 @@ #if defined(USE_TI_MEDIAGETAPPMUSICPLAYER) || defined(USE_TI_MEDIAOPENMUSICLIBRARY) || defined(USE_TI_MEDIAAPPMUSICPLAYER) || defined(USE_TI_MEDIAGETSYSTEMMUSICPLAYER) || defined(USE_TI_MEDIASYSTEMMUSICPLAYER) || defined(USE_TI_MEDIAHASMUSICLIBRARYPERMISSIONS) #import #endif +#if IS_SDK_IOS_14 && defined(USE_TI_MEDIAOPENPHOTOGALLERY) +#import +#endif #import "TiMediaAudioSession.h" #import "TiMediaMusicPlayer.h" #import "TiMediaTypes.h" @@ -17,12 +20,14 @@ #import @class AVAudioRecorder; - @interface MediaModule : TiModule < UINavigationControllerDelegate, #if defined(USE_TI_MEDIASHOWCAMERA) || defined(USE_TI_MEDIAOPENPHOTOGALLERY) || defined(USE_TI_MEDIASTARTVIDEOEDITING) UIImagePickerControllerDelegate, #endif +#if IS_SDK_IOS_14 && defined(USE_TI_MEDIAOPENPHOTOGALLERY) + PHPickerViewControllerDelegate, +#endif #ifdef USE_TI_MEDIAOPENMUSICLIBRARY MPMediaPickerControllerDelegate, #endif @@ -36,6 +41,9 @@ // Camera picker #if defined(USE_TI_MEDIASHOWCAMERA) || defined(USE_TI_MEDIAOPENPHOTOGALLERY) || defined(USE_TI_MEDIASTARTVIDEOEDITING) UIImagePickerController *picker; +#endif +#if IS_SDK_IOS_14 && defined(USE_TI_MEDIAOPENPHOTOGALLERY) + PHPickerViewController *_phPicker; #endif BOOL autoHidePicker; BOOL saveToRoll; diff --git a/iphone/Classes/MediaModule.m b/iphone/Classes/MediaModule.m index 3cd3f5f6991..96ea65400f4 100644 --- a/iphone/Classes/MediaModule.m +++ b/iphone/Classes/MediaModule.m @@ -36,6 +36,7 @@ #ifdef USE_TI_MEDIAVIDEOPLAYER #import "TiMediaVideoPlayerProxy.h" #endif +#import // by default, we want to make the camera fullscreen and // these transform values will scale it when we have our own overlay @@ -1107,8 +1108,20 @@ - (void)openPhotoGallery:(id)args { ENSURE_SINGLE_ARG_OR_NIL(args, NSDictionary); ENSURE_UI_THREAD(openPhotoGallery, args); - [self showPicker:args isCamera:NO]; + + NSArray *types = (NSArray *)[args objectForKey:@"mediaTypes"]; +#if IS_SDK_IOS_14 + if ([TiUtils isIOSVersionOrGreater:@"14.0"] && [TiUtils boolValue:[args objectForKey:@"allowMultiple"] def:NO]) { + [self showPHPicker:args]; + } else { +#endif + [self showPicker:args + isCamera:NO]; +#if IS_SDK_IOS_14 + } +#endif } + #endif /** @@ -1378,6 +1391,12 @@ - (void)destroyPicker } RELEASE_TO_NIL(picker); #endif + +#if IS_SDK_IOS_14 && defined(USE_TI_MEDIAOPENPHOTOGALLERY) + _phPicker.presentationController.delegate = nil; + RELEASE_TO_NIL(_phPicker); +#endif + #if defined(USE_TI_MEDIASTARTVIDEOEDITING) || defined(USE_TI_MEDIASTOPVIDEOEDITING) RELEASE_TO_NIL(editor); #endif @@ -1763,6 +1782,175 @@ - (void)handleTrimmedVideo:(NSURL *)theURL withDictionary:(NSDictionary *)dictio } #endif +#if IS_SDK_IOS_14 +- (void)showPHPicker:(NSDictionary *)args +{ + if (_phPicker != nil) { + [self sendPickerError:MediaModuleErrorBusy]; + return; + } + + animatedPicker = YES; + + PHPickerConfiguration *configuration = [[PHPickerConfiguration alloc] init]; + NSMutableArray *filterList = [NSMutableArray array]; + + if (args != nil) { + [self commonPickerSetup:args]; + + BOOL allowMultiple = [TiUtils boolValue:[args objectForKey:@"allowMultiple"] def:NO]; + configuration.selectionLimit = [TiUtils intValue:[args objectForKey:@"selectionLimit"] def:allowMultiple ? 0 : 1]; + + NSArray *mediaTypes = (NSArray *)[args objectForKey:@"mediaTypes"]; + if (mediaTypes) { + for (NSString *mediaType in mediaTypes) { + if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) { + [filterList addObject:PHPickerFilter.imagesFilter]; + } else if ([mediaType isEqualToString:(NSString *)kUTTypeLivePhoto]) { + [filterList addObject:PHPickerFilter.livePhotosFilter]; + } else if ([mediaType isEqualToString:(NSString *)kUTTypeMovie]) { + [filterList addObject:PHPickerFilter.videosFilter]; + } + } + } + } + + if (filterList.count == 0) { + [filterList addObject:PHPickerFilter.imagesFilter]; + } + + PHPickerFilter *filter = [PHPickerFilter anyFilterMatchingSubfilters:filterList]; + configuration.filter = filter; + + _phPicker = [[PHPickerViewController alloc] initWithConfiguration:configuration]; + + [_phPicker setDelegate:self]; + [self displayModalPicker:_phPicker settings:args]; +} + +#pragma mark PHPickerViewControllerDelegate + +- (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray *)results +{ + // If user cancels, results count will be 0 + if (results.count == 0) { + [self closeModalPicker:picker]; + [self sendPickerCancel]; + return; + } + + dispatch_group_t group = dispatch_group_create(); + __block NSMutableArray *imageArray = nil; + __block NSMutableArray *livePhotoArray = nil; + __block NSMutableArray *videoArray = nil; + + for (PHPickerResult *result in results) { + dispatch_group_enter(group); + + if ([result.itemProvider canLoadObjectOfClass:PHLivePhoto.class]) { + if (!livePhotoArray) { + livePhotoArray = [[NSMutableArray alloc] init]; + } + [result.itemProvider loadObjectOfClass:PHLivePhoto.class + completionHandler:^(__kindof id _Nullable object, NSError *_Nullable error) { + if (!error) { + TiUIiOSLivePhoto *livePhoto = [[[TiUIiOSLivePhoto alloc] _initWithPageContext:[self pageContext]] autorelease]; + [livePhoto setLivePhoto:(PHLivePhoto *)object]; + [livePhotoArray addObject:@{@"livePhoto" : livePhoto, + @"mediaType" : (NSString *)kUTTypeLivePhoto, + @"success" : @(YES), + @"code" : @(0)}]; + } else { + [livePhotoArray addObject:@{@"error" : error.description, + @"code" : @(error.code), + @"success" : @(NO), + @"mediaType" : (NSString *)kUTTypeLivePhoto}]; + DebugLog(@"[ERROR] Failed to load live photo- %@ .", error.description); + } + dispatch_group_leave(group); + }]; + } else if ([result.itemProvider canLoadObjectOfClass:UIImage.class]) { + if (!imageArray) { + imageArray = [[NSMutableArray alloc] init]; + } + [result.itemProvider loadObjectOfClass:UIImage.class + completionHandler:^(__kindof id _Nullable object, NSError *_Nullable error) { + if (!error) { + TiBlob *media = [[[TiBlob alloc] initWithImage:(UIImage *)object] autorelease]; + [imageArray addObject:@{@"media" : media, + @"mediaType" : (NSString *)kUTTypeImage, + @"success" : @(YES), + @"code" : @(0)}]; + } else { + [imageArray addObject:@{@"error" : error.description, + @"code" : @(error.code), + @"success" : @(NO), + @"mediaType" : (NSString *)kUTTypeImage}]; + DebugLog(@"[ERROR] Failed to load image- %@ .", error.description); + } + dispatch_group_leave(group); + }]; + } else if ([result.itemProvider hasItemConformingToTypeIdentifier:UTTypeMovie.identifier]) { + if (!videoArray) { + videoArray = [[NSMutableArray alloc] init]; + } + [result.itemProvider loadFileRepresentationForTypeIdentifier:UTTypeMovie.identifier + completionHandler:^(NSURL *_Nullable url, NSError *_Nullable error) { + // As per discussion- https://developer.apple.com/forums/thread/652695 + if (!error) { + NSString *filename = url.lastPathComponent; + NSFileManager *fileManager = NSFileManager.defaultManager; + NSURL *destPath = [fileManager.temporaryDirectory URLByAppendingPathComponent:filename]; + + NSError *copyError; + + [fileManager copyItemAtURL:url + toURL:destPath + error:©Error]; + TiBlob *media = [[[TiBlob alloc] initWithFile:[destPath path]] autorelease]; + if ([media mimeType] == nil) { + [media setMimeType:@"video/mpeg" type:TiBlobTypeFile]; + } + [videoArray addObject:@{@"media" : media, + @"mediaType" : (NSString *)kUTTypeMovie, + @"success" : @(YES), + @"code" : @(0)}]; + } else { + [videoArray addObject:@{@"error" : error.description, + @"code" : @(error.code), + @"success" : @(NO), + @"mediaType" : (NSString *)kUTTypeMovie}]; + DebugLog(@"[ERROR] Failed to load video- %@ .", error.description); + } + dispatch_group_leave(group); + }]; + } else { + dispatch_group_leave(group); + NSLog(@"Unsupported media type"); + } + } + + dispatch_group_notify(group, dispatch_get_global_queue(QOS_CLASS_DEFAULT, DISPATCH_QUEUE_PRIORITY_DEFAULT), ^{ + // Perform completition block + NSMutableDictionary *dictionary = [TiUtils dictionaryWithCode:0 message:nil]; + if (livePhotoArray != nil) { + [dictionary setObject:livePhotoArray forKey:@"livePhotos"]; + } + if (imageArray != nil) { + [dictionary setObject:imageArray forKey:@"images"]; + } + if (videoArray != nil) { + [dictionary setObject:videoArray forKey:@"videos"]; + } + [self sendPickerSuccess:dictionary]; + }); + + if (autoHidePicker) { + [self closeModalPicker:picker]; + } +} +#endif + #pragma mark UIPopoverPresentationControllerDelegate - (void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController @@ -1826,7 +2014,7 @@ - (void)popoverPresentationControllerDidDismissPopover:(UIPopoverPresentationCon - (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController { #if defined(USE_TI_MEDIASHOWCAMERA) || defined(USE_TI_MEDIAOPENPHOTOGALLERY) || defined(USE_TI_MEDIASTARTVIDEOEDITING) - [self closeModalPicker:picker]; + [self closeModalPicker:picker ?: _phPicker]; [self sendPickerCancel]; #endif }