Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ios): allow multiple photo selection #11867

Merged
merged 18 commits into from
Sep 14, 2020
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
48 changes: 45 additions & 3 deletions apidoc/Titanium/Media/Media.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2054,7 +2054,9 @@ summary: |
properties:
- name: success
summary: Function to call when the photo gallery is closed after a successful selection.
type: Callback<CameraMediaItemType>
description: |
If <Titanium.Media.allowMultiple> is `true` or <Titanium.Media.selectionLimit> is set, Callback<CameraMediaMultipleItemsType> is called otherwise Callback<CameraMediaItemType>.
vijaysingh-axway marked this conversation as resolved.
Show resolved Hide resolved
type: [Callback<CameraMediaItemType>, Callback<CameraMediaMultipleItemsType>]
- name: error
summary: Function to call upon receiving an error.
type: Callback<FailureResponse>
Expand Down Expand Up @@ -2104,8 +2106,15 @@ 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 assets that can be selected.
type: Boolean
platforms: [iphone, ipad]
osver: {ios: {min: "14.0"}}
since: "9.2.0"
jquick-axway marked this conversation as resolved.
Show resolved Hide resolved
- 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
Expand All @@ -2115,6 +2124,39 @@ properties:
osver: {ios: {min: "11.0"}}

---
name: CameraMediaMultipleItemsType
summary: A media object from photo gallery when <Titanium.Media.allowMultiple> is `true` or in iOS <Titanium.Media.selectionLimit> is set.
extends: SuccessResponse
properties:
- name: success
sgtcoolguy marked this conversation as resolved.
Show resolved Hide resolved
summary: Indicates if the operation succeeded. Returns `true`.
description: Returns `true`.
type: Boolean
- name: error
summary: Error message, if any returned.
description: Will be undefined.
type: String
- name: code
summary: Error code. Returns 0.
description: Error code will be 0.
type: Number
- name: images
summary: |
The list of selected images.
type: Array<CameraMediaItemType>
optional: true
- name: livePhotos
summary: |
The list of selected live photo objects.
type: Array<Titanium.UI.iOS.LivePhoto>
platforms: [iphone, ipad]
optional: true
- name: videos
summary: |
The list of selected videos.
type: Array<CameraMediaItemType>
optional: true
---
name: CameraMediaItemType
summary: A media object from the camera or photo gallery.
extends: SuccessResponse
Expand Down
16 changes: 15 additions & 1 deletion iphone/Classes/MediaModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
#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 <MediaPlayer/MediaPlayer.h>
#endif
#if IS_SDK_IOS_14
sgtcoolguy marked this conversation as resolved.
Show resolved Hide resolved
#if defined(USE_TI_MEDIAOPENPHOTOGALLERY)
#import <PhotosUI/PHPicker.h>
#endif
#endif
#import "TiMediaAudioSession.h"
#import "TiMediaMusicPlayer.h"
#import "TiMediaTypes.h"
Expand All @@ -17,12 +22,16 @@
#import <TitaniumKit/TiViewProxy.h>

@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
sgtcoolguy marked this conversation as resolved.
Show resolved Hide resolved
#if defined(USE_TI_MEDIAOPENPHOTOGALLERY)
PHPickerViewControllerDelegate,
#endif
#endif
#ifdef USE_TI_MEDIAOPENMUSICLIBRARY
MPMediaPickerControllerDelegate,
#endif
Expand All @@ -36,6 +45,11 @@
// Camera picker
#if defined(USE_TI_MEDIASHOWCAMERA) || defined(USE_TI_MEDIAOPENPHOTOGALLERY) || defined(USE_TI_MEDIASTARTVIDEOEDITING)
UIImagePickerController *picker;
#endif
#if IS_SDK_IOS_14
sgtcoolguy marked this conversation as resolved.
Show resolved Hide resolved
#if defined(USE_TI_MEDIAOPENPHOTOGALLERY)
PHPickerViewController *_phPicker;
#endif
#endif
BOOL autoHidePicker;
BOOL saveToRoll;
Expand Down
194 changes: 192 additions & 2 deletions iphone/Classes/MediaModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#ifdef USE_TI_MEDIAVIDEOPLAYER
#import "TiMediaVideoPlayerProxy.h"
#endif
#import <UniformTypeIdentifiers/UTCoreTypes.h>

// by default, we want to make the camera fullscreen and
// these transform values will scale it when we have our own overlay
Expand Down Expand Up @@ -1105,8 +1106,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"] && ([args objectForKey:@"selectionLimit"] || [TiUtils boolValue:[args objectForKey:@"allowMultiple"] def:NO])) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we maybe always use it on iOS 14? The old image picker is now deprecated and should be replaced for iOS 14+.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In new PHPickerViewController, there is no option for image editing(may be included in future releases). I think this is important feature and we can not remove it in minor release.

jquick-axway marked this conversation as resolved.
Show resolved Hide resolved
[self showPHPicker:args];
} else {
#endif
[self showPicker:args
isCamera:NO];
#if IS_SDK_IOS_14
}
#endif
}

#endif

/**
Expand Down Expand Up @@ -1376,6 +1389,14 @@ - (void)destroyPicker
}
RELEASE_TO_NIL(picker);
#endif

#if IS_SDK_IOS_14
#if defined(USE_TI_MEDIAOPENPHOTOGALLERY)
sgtcoolguy marked this conversation as resolved.
Show resolved Hide resolved
_phPicker.presentationController.delegate = nil;
RELEASE_TO_NIL(_phPicker);
#endif
#endif

#if defined(USE_TI_MEDIASTARTVIDEOEDITING) || defined(USE_TI_MEDIASTOPVIDEOEDITING)
RELEASE_TO_NIL(editor);
#endif
Expand Down Expand Up @@ -1761,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<PHPickerResult *> *)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<NSDictionary *> *imageArray = nil;
__block NSMutableArray<NSDictionary *> *livePhotoArray = nil;
__block NSMutableArray<NSDictionary *> *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<NSItemProviderReading> _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<NSItemProviderReading> _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:&copyError];
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];
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are on a background thread here. Maybe validate if closeModalPicker and sendPickerSuccess really are performed on the main thread. In any case, there doesn't seem to be much processing here, so is a global queue / background queue necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no special handling in this delegate method for closeModalPicker and sendPickerSuccess. It is same as with old picker. In dispatch_group_notify queue is mandatory to pass.

#endif

#pragma mark UIPopoverPresentationControllerDelegate

- (void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
Expand Down Expand Up @@ -1824,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
}
Expand Down