element-ios/Riot/Modules/MediaPicker/MediaPickerViewController.m

1092 lines
42 KiB
Mathematica
Raw Normal View History

2015-08-18 08:04:30 +00:00
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
2015-08-18 08:04:30 +00:00
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MediaPickerViewController.h"
#import "Riot-Swift.h"
#import <Photos/Photos.h>
#import <AVKit/AVKit.h>
2017-02-28 09:24:27 +00:00
#import <MobileCoreServices/MobileCoreServices.h>
2016-01-19 17:59:40 +00:00
#import "MediaAlbumContentViewController.h"
#import "MediaAlbumTableCell.h"
#import <MatrixKit/MatrixKit.h>
@interface MediaPickerViewController () <UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout, MediaAlbumContentViewControllerDelegate>
2015-08-18 08:04:30 +00:00
{
2016-01-19 17:59:40 +00:00
/**
Observe UIApplicationWillEnterForegroundNotification to refresh captures collection when app leaves the background state.
2016-01-19 17:59:40 +00:00
*/
id UIApplicationWillEnterForegroundNotificationObserver;
PHFetchResult *recentCaptures;
/**
User's albums
*/
dispatch_queue_t userAlbumsQueue;
NSArray *userAlbums;
MXKImageView* validationView;
AVPlayerViewController *videoPlayer;
UIButton *videoPlayerControl;
BOOL isValidationInProgress;
/**
Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change.
*/
id kThemeServiceDidChangeThemeNotificationObserver;
/**
The current visibility of the status bar in this view controller.
*/
BOOL isStatusBarHidden;
2015-08-18 08:04:30 +00:00
}
@property (weak, nonatomic) IBOutlet UIScrollView *mainScrollView;
@property (weak, nonatomic) IBOutlet UIView *recentCapturesCollectionContainerView;
@property (weak, nonatomic) IBOutlet UICollectionView *recentCapturesCollectionView;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *recentCapturesCollectionContainerViewHeightConstraint;
@property (weak, nonatomic) IBOutlet UIView *libraryViewContainer;
@property (weak, nonatomic) IBOutlet UITableView *userAlbumsTableView;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *libraryViewContainerViewHeightConstraint;
2015-08-18 08:04:30 +00:00
@end
@implementation MediaPickerViewController
#pragma mark - Class methods
+ (instancetype)instantiate
2015-08-18 08:04:30 +00:00
{
return [[[self class] alloc] initWithNibName:NSStringFromClass([MediaPickerViewController class])
bundle:[NSBundle bundleForClass:[MediaPickerViewController class]]];
}
#pragma mark -
- (void)finalizeInit
2015-08-18 08:04:30 +00:00
{
[super finalizeInit];
2015-08-18 08:51:28 +00:00
// Setup `MXKViewControllerHandling` properties
self.enableBarTintColorStatusChange = NO;
self.rageShakeManager = [RageShakeManager sharedManager];
// Keep visible the status bar by default.
isStatusBarHidden = NO;
}
- (void)dealloc
{
if (kThemeServiceDidChangeThemeNotificationObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:kThemeServiceDidChangeThemeNotificationObserver];
kThemeServiceDidChangeThemeNotificationObserver = nil;
}
if (UIApplicationWillEnterForegroundNotificationObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:UIApplicationWillEnterForegroundNotificationObserver];
UIApplicationWillEnterForegroundNotificationObserver = nil;
}
[self dismissImageValidationView];
userAlbumsQueue = nil;
userAlbums = nil;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.title = NSLocalizedStringFromTable(@"media_picker_title", @"Vector", nil);
MXWeakify(self);
UIBarButtonItem *closeBarButtonItem = [[MXKBarButtonItem alloc] initWithTitle:NSLocalizedStringFromTable(@"cancel", @"Vector", nil) style:UIBarButtonItemStylePlain action:^{
MXStrongifyAndReturnIfNil(self);
[self.delegate mediaPickerControllerDidCancel:self];
}];
self.navigationItem.rightBarButtonItem = closeBarButtonItem;
// Register collection view cell class
[self.recentCapturesCollectionView registerNib:MXKMediaCollectionViewCell.nib forCellWithReuseIdentifier:[MXKMediaCollectionViewCell defaultReuseIdentifier]];
// Register album table view cell class
[self.userAlbumsTableView registerNib:MediaAlbumTableCell.nib forCellReuseIdentifier:[MediaAlbumTableCell defaultReuseIdentifier]];
self.userAlbumsTableView.alwaysBounceVertical = NO;
// Force UI refresh according to selected media types - Set default media type if none.
self.mediaTypes = _mediaTypes ? _mediaTypes : @[(NSString *)kUTTypeImage];
// Check photo library access
[self checkPhotoLibraryAuthorizationStatus];
2016-01-19 17:59:40 +00:00
// Observe UIApplicationWillEnterForegroundNotification to refresh captures collection when app leaves the background state.
UIApplicationWillEnterForegroundNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillEnterForegroundNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
MXStrongifyAndReturnIfNil(self);
2016-01-19 17:59:40 +00:00
[self reloadRecentCapturesCollection];
[self reloadUserLibraryAlbums];
2016-01-19 17:59:40 +00:00
}];
// Observe user interface theme change.
kThemeServiceDidChangeThemeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kThemeServiceDidChangeThemeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
MXStrongifyAndReturnIfNil(self);
[self userInterfaceThemeDidChange];
}];
[self userInterfaceThemeDidChange];
}
- (void)userInterfaceThemeDidChange
{
2019-01-11 10:45:27 +00:00
[ThemeService.shared.theme applyStyleOnNavigationBar:self.navigationController.navigationBar];
2019-01-11 10:45:27 +00:00
self.activityIndicator.backgroundColor = ThemeService.shared.theme.overlayBackgroundColor;
2019-01-11 10:45:27 +00:00
self.userAlbumsTableView.backgroundColor = ThemeService.shared.theme.backgroundColor;
self.view.backgroundColor = ThemeService.shared.theme.backgroundColor;
self.recentCapturesCollectionContainerView.backgroundColor = ThemeService.shared.theme.backgroundColor;
self.recentCapturesCollectionView.backgroundColor = ThemeService.shared.theme.backgroundColor;
2019-02-18 11:53:13 +00:00
self.userAlbumsTableView.separatorColor = ThemeService.shared.theme.lineBreakColor;
[self setNeedsStatusBarAppearanceUpdate];
}
- (UIStatusBarStyle)preferredStatusBarStyle
{
2019-01-11 10:45:27 +00:00
return ThemeService.shared.theme.statusBarStyle;
2015-08-18 08:04:30 +00:00
}
- (BOOL)prefersStatusBarHidden
{
// Return the current status bar visibility.
return isStatusBarHidden;
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
[self updateRecentCapturesCollectionViewHeightIfNeeded];
2015-08-18 08:04:30 +00:00
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self userInterfaceThemeDidChange];
// Screen tracking
[[Analytics sharedInstance] trackScreen:@"MediaPicker"];
if (!userAlbumsQueue)
{
userAlbumsQueue = dispatch_queue_create("media.picker.user.albums", DISPATCH_QUEUE_SERIAL);
}
2016-01-19 17:59:40 +00:00
[self reloadRecentCapturesCollection];
[self reloadUserLibraryAlbums];
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(coordinator.transitionDuration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self updateRecentCapturesCollectionViewHeightIfNeeded];
});
}
- (void)checkPhotoLibraryAuthorizationStatus
{
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
switch (status) {
case PHAuthorizationStatusAuthorized:
// Load recent captures if this is not already done
if (!self->recentCaptures.count)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self reloadRecentCapturesCollection];
[self reloadUserLibraryAlbums];
});
}
break;
default:
2016-01-19 17:59:40 +00:00
dispatch_async(dispatch_get_main_queue(), ^{
[self presentPermissionDeniedAlert];
2016-01-19 17:59:40 +00:00
});
break;
2016-01-19 17:59:40 +00:00
}
}];
}
- (void)presentPermissionDeniedAlert
{
NSString *appDisplayName = [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"];
NSString *message = [NSString stringWithFormat:NSLocalizedStringFromTable(@"photo_library_access_not_granted", @"Vector", nil), appDisplayName];
UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedStringFromTable(@"media_picker_title", @"Vector", nil)
message:message
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"]
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * action) {
[self.delegate mediaPickerControllerDidCancel:self];
}]];
NSURL *settingsURL = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
[alert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"settings"]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[UIApplication.sharedApplication openURL:settingsURL options:@{} completionHandler:^(BOOL success) {
if (success)
{
[self.delegate mediaPickerControllerDidCancel:self];
}
else
{
MXLogDebug(@"[MediaPickerVC] Fails to open settings");
}
}];
}]];
[self presentViewController:alert animated:YES completion:nil];
}
#pragma mark -
- (void)setMediaTypes:(NSArray *)mediaTypes
{
if (_mediaTypes != mediaTypes)
{
_mediaTypes = mediaTypes;
[self reloadRecentCapturesCollection];
[self reloadUserLibraryAlbums];
}
}
#pragma mark - UI Refresh/Update
2016-01-19 17:59:40 +00:00
- (void)updateRecentCapturesCollectionViewHeightIfNeeded
2016-01-20 10:58:30 +00:00
{
// Update Captures collection display
if (recentCaptures.count)
{
// recents Collection is limited to the first 12 assets
NSInteger recentsCount = ((recentCaptures.count > 12) ? 12 : recentCaptures.count);
CGFloat collectionViewHeight = (ceil(recentsCount / 4.0) * ((self.view.frame.size.width - 6) / 4)) + 10;
if (self.recentCapturesCollectionContainerViewHeightConstraint.constant != collectionViewHeight)
{
self.recentCapturesCollectionContainerViewHeightConstraint.constant = collectionViewHeight;
[self.recentCapturesCollectionView reloadData];
}
}
else
{
self.recentCapturesCollectionContainerViewHeightConstraint.constant = 0;
2016-01-20 10:58:30 +00:00
}
}
2016-01-19 17:59:40 +00:00
- (void)reloadRecentCapturesCollection
{
// Retrieve recents snapshot for the selected media types
PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeSmartAlbumUserLibrary options:nil];
// Only one album is expected
if (smartAlbums.count)
{
2016-01-19 17:59:40 +00:00
// Set up fetch options.
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
if ([_mediaTypes indexOfObject:(NSString *)kUTTypeImage] != NSNotFound)
{
if ([_mediaTypes indexOfObject:(NSString *)kUTTypeMovie] != NSNotFound)
{
options.predicate = [NSPredicate predicateWithFormat:@"(mediaType == %d) || (mediaType == %d)", PHAssetMediaTypeImage, PHAssetMediaTypeVideo];
}
else
{
options.predicate = [NSPredicate predicateWithFormat:@"mediaType == %d",PHAssetMediaTypeImage];
}
}
else if ([_mediaTypes indexOfObject:(NSString *)kUTTypeMovie] != NSNotFound)
{
options.predicate = [NSPredicate predicateWithFormat:@"mediaType == %d",PHAssetMediaTypeVideo];
}
// fetchLimit is available for iOS 9.0 and later
if ([options respondsToSelector:@selector(fetchLimit)])
{
options.fetchLimit = 12;
}
PHAssetCollection *assetCollection = smartAlbums[0];
2016-01-19 17:59:40 +00:00
recentCaptures = [PHAsset fetchAssetsInAssetCollection:assetCollection options:options];
MXLogDebug(@"[MediaPickerVC] lists %tu assets that were recently added to the photo library", recentCaptures.count);
2016-01-19 17:59:40 +00:00
}
else
{
recentCaptures = nil;
}
2016-01-19 17:59:40 +00:00
if (recentCaptures.count)
{
2016-01-19 17:59:40 +00:00
self.recentCapturesCollectionView.hidden = NO;
[self.recentCapturesCollectionView reloadData];
}
else
{
2016-01-19 17:59:40 +00:00
self.recentCapturesCollectionView.hidden = YES;
}
// Force call updateRecentCapturesCollectionViewHeightIfNeeded
[self.recentCapturesCollectionContainerView setNeedsLayout];
[self.recentCapturesCollectionContainerView layoutIfNeeded];
}
2016-01-19 17:59:40 +00:00
- (void)reloadUserLibraryAlbums
{
// Sanity check
if (!userAlbumsQueue)
{
return;
}
MXWeakify(self);
dispatch_async(userAlbumsQueue, ^{
MXStrongifyAndReturnIfNil(self);
// List user albums which are not empty
PHFetchResult *albums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];
NSMutableArray *updatedUserAlbums = [NSMutableArray array];
__block PHAssetCollection *cameraRollAlbum, *videoAlbum;
// Set up fetch options.
PHFetchOptions *options = [[PHFetchOptions alloc] init];
if ([self->_mediaTypes indexOfObject:(NSString *)kUTTypeImage] != NSNotFound)
{
if ([self->_mediaTypes indexOfObject:(NSString *)kUTTypeMovie] != NSNotFound)
{
options.predicate = [NSPredicate predicateWithFormat:@"(mediaType == %d) || (mediaType == %d)", PHAssetMediaTypeImage, PHAssetMediaTypeVideo];
}
else
{
options.predicate = [NSPredicate predicateWithFormat:@"mediaType == %d",PHAssetMediaTypeImage];
}
2016-01-19 17:59:40 +00:00
}
else if ([self->_mediaTypes indexOfObject:(NSString *)kUTTypeMovie] != NSNotFound)
2016-01-19 17:59:40 +00:00
{
options.predicate = [NSPredicate predicateWithFormat:@"mediaType == %d",PHAssetMediaTypeVideo];
}
2016-01-19 17:59:40 +00:00
[albums enumerateObjectsUsingBlock:^(PHAssetCollection *collection, NSUInteger idx, BOOL *stop) {
PHFetchResult *assets = [PHAsset fetchAssetsInAssetCollection:collection options:options];
MXLogDebug(@"album title %@, estimatedAssetCount %tu", collection.localizedTitle, assets.count);
if (assets.count)
{
if (collection.assetCollectionSubtype == PHAssetCollectionSubtypeSmartAlbumUserLibrary)
{
cameraRollAlbum = collection;
}
else if (collection.assetCollectionSubtype == PHAssetCollectionSubtypeSmartAlbumVideos)
{
videoAlbum = collection;
}
else
{
[updatedUserAlbums addObject:collection];
}
}
}];
2016-01-19 17:59:40 +00:00
// Move the camera roll at the top, followed by video and the rest by default
if (videoAlbum)
{
[updatedUserAlbums insertObject:videoAlbum atIndex:0];
}
if (cameraRollAlbum)
{
[updatedUserAlbums insertObject:cameraRollAlbum atIndex:0];
}
dispatch_async(dispatch_get_main_queue(), ^{
self->userAlbums = updatedUserAlbums;
if (self->userAlbums.count)
{
self.userAlbumsTableView.hidden = NO;
self.libraryViewContainerViewHeightConstraint.constant = (self->userAlbums.count * 74);
[self.libraryViewContainer needsUpdateConstraints];
[self.userAlbumsTableView reloadData];
}
else
{
self.userAlbumsTableView.hidden = YES;
self.libraryViewContainerViewHeightConstraint.constant = 0;
}
});
});
2016-01-19 17:59:40 +00:00
}
#pragma mark - Validation step
2016-01-19 17:59:40 +00:00
- (void)didSelectAsset:(PHAsset *)asset
{
// Check whether a selection is already in progress
if (isValidationInProgress)
{
return;
}
if (asset.mediaType == PHAssetMediaTypeImage)
{
isValidationInProgress = YES;
PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
options.synchronous = NO;
options.networkAccessAllowed = YES;
id topVC = self.navigationController.topViewController;
if ([topVC respondsToSelector:@selector(startActivityIndicator)])
{
[topVC startActivityIndicator];
}
[[PHImageManager defaultManager] requestImageForAsset:asset targetSize:self.view.frame.size contentMode:PHImageContentModeAspectFill options:options resultHandler:^(UIImage *result, NSDictionary *info) {
if ([topVC respondsToSelector:@selector(stopActivityIndicator)])
{
[topVC stopActivityIndicator];
}
if (result)
{
// Validate the selection
[self validateSelectedImage:result responseHandler:^(BOOL isValidated) {
if (isValidated)
{
// Note we can use `options.progressHandler` to display an animation during the potential download.
if ([topVC respondsToSelector:@selector(startActivityIndicator)])
{
[topVC startActivityIndicator];
}
[[PHImageManager defaultManager] requestImageDataForAsset:asset options:options resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) {
if ([topVC respondsToSelector:@selector(stopActivityIndicator)])
{
[topVC stopActivityIndicator];
}
if (imageData)
{
MXLogDebug(@"[MediaPickerVC] didSelectAsset: Got image data");
CFStringRef uti = (__bridge CFStringRef)dataUTI;
NSString *mimeType = (__bridge_transfer NSString *) UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType);
// Send the original image
[self.delegate mediaPickerController:self didSelectImage:imageData withMimeType:mimeType isPhotoLibraryAsset:YES];
}
else
{
MXLogDebug(@"[MediaPickerVC] didSelectAsset: Failed to get image data for asset");
// Alert user
NSError *error = info[@"PHImageErrorKey"];
if (error.userInfo[NSUnderlyingErrorKey])
{
error = error.userInfo[NSUnderlyingErrorKey];
}
[[AppDelegate theDelegate] showErrorAsAlert:error];
}
}];
}
self->isValidationInProgress = NO;
}];
}
else
{
MXLogDebug(@"[MediaPickerVC] didSelectAsset: Failed to get image for asset");
self->isValidationInProgress = NO;
// Alert user
NSError *error = info[@"PHImageErrorKey"];
if (error.userInfo[NSUnderlyingErrorKey])
{
error = error.userInfo[NSUnderlyingErrorKey];
}
[[AppDelegate theDelegate] showErrorAsAlert:error];
}
}];
}
else if (asset.mediaType == PHAssetMediaTypeVideo)
{
isValidationInProgress = YES;
PHVideoRequestOptions *options = [[PHVideoRequestOptions alloc] init];
options.networkAccessAllowed = YES;
id topVC = self.navigationController.topViewController;
if ([topVC respondsToSelector:@selector(startActivityIndicator)])
{
[topVC startActivityIndicator];
}
[[PHImageManager defaultManager] requestAVAssetForVideo:asset options:options resultHandler:^(AVAsset * _Nullable asset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) {
dispatch_async(dispatch_get_main_queue(), ^{
if ([topVC respondsToSelector:@selector(stopActivityIndicator)])
{
[topVC stopActivityIndicator];
}
if (asset)
{
if ([asset isKindOfClass:[AVURLAsset class]])
{
MXLogDebug(@"[MediaPickerVC] didSelectAsset: Got AVAsset for video");
AVURLAsset *avURLAsset = (AVURLAsset*)asset;
// Validate first the selected video
[self validateSelectedVideo:[avURLAsset URL] responseHandler:^(BOOL isValidated) {
if (isValidated)
{
[self.delegate mediaPickerController:self didSelectVideo:[avURLAsset URL]];
}
self->isValidationInProgress = NO;
}];
}
else
{
MXLogDebug(@"[MediaPickerVC] Selected video asset is not initialized from an URL!");
self->isValidationInProgress = NO;
}
}
else
{
MXLogDebug(@"[MediaPickerVC] didSelectAsset: Failed to get image for asset");
self->isValidationInProgress = NO;
// Alert user
NSError *error = info[@"PHImageErrorKey"];
if (error.userInfo[NSUnderlyingErrorKey])
{
error = error.userInfo[NSUnderlyingErrorKey];
}
[[AppDelegate theDelegate] showErrorAsAlert:error];
}
});
}];
}
else
{
MXLogDebug(@"[MediaPickerVC] didSelectAsset: Unexpected media type");
}
2016-01-19 17:59:40 +00:00
}
- (void)validateSelectedImage:(UIImage*)selectedImage responseHandler:(void (^)(BOOL isValidated))handler
{
[self dismissImageValidationView];
2016-01-19 17:59:40 +00:00
// Add a preview to let the user validates his selection
__weak typeof(self) weakSelf = self;
validationView = [[MXKImageView alloc] initWithFrame:CGRectZero];
validationView.stretchable = YES;
// the user validates the image
[validationView setRightButtonTitle:[NSBundle mxk_localizedStringForKey:@"ok"] handler:^(MXKImageView* imageView, NSString* buttonTitle) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
// Dismiss the image view
[strongSelf dismissImageValidationView];
handler (YES);
}];
// the user wants to use an other image
[validationView setLeftButtonTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] handler:^(MXKImageView* imageView, NSString* buttonTitle) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
// dismiss the image view
[strongSelf dismissImageValidationView];
handler (NO);
}];
validationView.image = selectedImage;
[validationView showFullScreen];
// Hide the status bar
isStatusBarHidden = YES;
// Trigger status bar update
[self setNeedsStatusBarAppearanceUpdate];
}
- (void)validateSelectedVideo:(NSURL*)selectedVideoURL responseHandler:(void (^)(BOOL isValidated))handler
{
[self dismissImageValidationView];
// Add a preview to let the user validates his selection
__weak typeof(self) weakSelf = self;
validationView = [[MXKImageView alloc] initWithFrame:CGRectZero];
validationView.stretchable = NO;
2016-01-19 17:59:40 +00:00
// the user validates the image
[validationView setRightButtonTitle:[NSBundle mxk_localizedStringForKey:@"ok"] handler:^(MXKImageView* imageView, NSString* buttonTitle) {
2016-01-19 17:59:40 +00:00
__strong __typeof(weakSelf)strongSelf = weakSelf;
// Dismiss the image view
[strongSelf dismissImageValidationView];
handler (YES);
}];
// the user wants to use an other image
[validationView setLeftButtonTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] handler:^(MXKImageView* imageView, NSString* buttonTitle) {
2016-01-19 17:59:40 +00:00
__strong __typeof(weakSelf)strongSelf = weakSelf;
// dismiss the image view
[strongSelf dismissImageValidationView];
handler (NO);
}];
// Display first video frame
videoPlayer = [[AVPlayerViewController alloc] init];
if (videoPlayer)
{
videoPlayer.allowsPictureInPicturePlayback = NO;
videoPlayer.updatesNowPlayingInfoCenter = NO;
videoPlayer.player = [AVPlayer playerWithURL:selectedVideoURL];
videoPlayer.videoGravity = AVLayerVideoGravityResizeAspect;
videoPlayer.showsPlaybackControls = NO;
// create a thumbnail for the first frame
AVAsset *asset = [AVAsset assetWithURL:selectedVideoURL];
AVAssetImageGenerator *generator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];
generator.appliesPreferredTrackTransform = YES;
CGImageRef thumbnailRef = [generator copyCGImageAtTime:kCMTimeZero actualTime:nil error:nil];
// set thumbnail on validationView
validationView.image = [UIImage imageWithCGImage:thumbnailRef];
}
[validationView showFullScreen];
// Now, there is a thumbnail, show the video control
videoPlayerControl = [UIButton buttonWithType:UIButtonTypeCustom];
[videoPlayerControl addTarget:self action:@selector(controlVideoPlayer) forControlEvents:UIControlEventTouchUpInside];
videoPlayerControl.frame = CGRectMake(0, 0, 44, 44);
[videoPlayerControl setImage:[UIImage imageNamed:@"camera_play"] forState:UIControlStateNormal];
[videoPlayerControl setImage:[UIImage imageNamed:@"camera_play"] forState:UIControlStateHighlighted];
[validationView addSubview:videoPlayerControl];
videoPlayerControl.center = validationView.imageView.center;
videoPlayerControl.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *centerXConstraint = [NSLayoutConstraint constraintWithItem:videoPlayerControl
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:validationView.imageView
attribute:NSLayoutAttributeCenterX
multiplier:1
constant:0.0f];
NSLayoutConstraint *centerYConstraint = [NSLayoutConstraint constraintWithItem:videoPlayerControl
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:validationView.imageView
attribute:NSLayoutAttributeCenterY
multiplier:1
constant:0.0f];
[NSLayoutConstraint activateConstraints:@[centerXConstraint, centerYConstraint]];
// Hide the status bar
isStatusBarHidden = YES;
// Trigger status bar update
[self setNeedsStatusBarAppearanceUpdate];
2016-01-19 17:59:40 +00:00
}
- (void)dismissImageValidationView
{
if (validationView)
2016-01-19 17:59:40 +00:00
{
if (videoPlayer)
{
[videoPlayer.player pause];
videoPlayer.player = nil;
[videoPlayer.view removeFromSuperview];
videoPlayer = nil;
[videoPlayerControl removeFromSuperview];
videoPlayerControl = nil;
}
[validationView dismissSelection];
[validationView removeFromSuperview];
validationView = nil;
// Restore the status bar
isStatusBarHidden = NO;
[self setNeedsStatusBarAppearanceUpdate];
}
}
- (void)controlVideoPlayer
{
// Check whether the video player is already playing
if (videoPlayer.view.superview)
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
[videoPlayer.player pause];
[videoPlayer.player seekToTime:kCMTimeZero];
[videoPlayer.view removeFromSuperview];
[videoPlayerControl setImage:[UIImage imageNamed:@"camera_play"] forState:UIControlStateNormal];
[videoPlayerControl setImage:[UIImage imageNamed:@"camera_play"] forState:UIControlStateHighlighted];
}
else
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(moviePlayerPlaybackDidFinishNotification:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:nil];
CGRect frame = validationView.imageView.frame;
frame.origin = CGPointZero;
videoPlayer.view.frame = frame;
videoPlayer.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[validationView.imageView addSubview:videoPlayer.view];
[videoPlayer.player play];
[videoPlayerControl setImage:[UIImage imageNamed:@"camera_stop"] forState:UIControlStateNormal];
[videoPlayerControl setImage:[UIImage imageNamed:@"camera_stop"] forState:UIControlStateHighlighted];
[validationView bringSubviewToFront:videoPlayerControl];
2016-01-19 17:59:40 +00:00
}
}
#pragma mark - Action
2015-08-18 08:51:28 +00:00
#pragma mark - UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
2016-01-19 17:59:40 +00:00
// Collection is limited to the first 12 assets
return ((recentCaptures.count > 12) ? 12 : recentCaptures.count);
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
2016-01-19 17:59:40 +00:00
MXKMediaCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[MXKMediaCollectionViewCell defaultReuseIdentifier] forIndexPath:indexPath];
2016-01-19 17:59:40 +00:00
// Sanity check: cancel pending asynchronous request (if any)
if (cell.tag)
{
2016-01-19 17:59:40 +00:00
[[PHImageManager defaultManager] cancelImageRequest:(PHImageRequestID)cell.tag];
cell.tag = 0;
}
if (indexPath.item < recentCaptures.count)
{
PHAsset *asset = recentCaptures[indexPath.item];
2016-01-19 17:59:40 +00:00
CGFloat collectionViewSquareSize = ((collectionView.frame.size.width - 6) / 4); // Here 6 = 3 * cell margin (= 2).
CGSize cellSize = CGSizeMake(collectionViewSquareSize, collectionViewSquareSize);
PHImageRequestOptions *option = [[PHImageRequestOptions alloc] init];
2016-01-19 17:59:40 +00:00
option.synchronous = NO;
cell.tag = [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:cellSize contentMode:PHImageContentModeAspectFill options:option resultHandler:^(UIImage *result, NSDictionary *info) {
cell.mxkImageView.imageView.contentMode = UIViewContentModeScaleAspectFill;
cell.mxkImageView.image = result;
2016-01-19 17:59:40 +00:00
cell.tag = 0;
}];
cell.bottomLeftIcon.image = [UIImage imageNamed:@"video_icon"];
cell.bottomLeftIcon.hidden = (asset.mediaType == PHAssetMediaTypeImage);
// Disable user interaction in mxkImageView, in order to let collection handle user selection
cell.mxkImageView.userInteractionEnabled = NO;
}
return cell;
}
#pragma mark - UICollectionViewDelegate
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
2016-01-19 17:59:40 +00:00
if (indexPath.item < recentCaptures.count)
{
2016-01-19 17:59:40 +00:00
[self didSelectAsset: recentCaptures[indexPath.item]];
}
}
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(nonnull UICollectionViewCell *)cell forItemAtIndexPath:(nonnull NSIndexPath *)indexPath
{
// Check whether a asynchronous request is pending
if (cell.tag)
{
[[PHImageManager defaultManager] cancelImageRequest:(PHImageRequestID)cell.tag];
cell.tag = 0;
}
}
#pragma mark - UICollectionViewDelegateFlowLayout
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
2016-01-19 17:59:40 +00:00
if (indexPath.item < recentCaptures.count)
{
2016-01-19 17:59:40 +00:00
CGFloat collectionViewSquareSize = ((collectionView.frame.size.width - 6) / 4); // Here 6 = 3 * cell margin (= 2).
CGSize cellSize = CGSizeMake(collectionViewSquareSize, collectionViewSquareSize);
return cellSize;
}
return CGSizeZero;
}
2016-01-19 17:59:40 +00:00
#pragma mark - UITableViewDataSource
2016-01-19 17:59:40 +00:00
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
2016-01-19 17:59:40 +00:00
return userAlbums.count;
}
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(nonnull NSIndexPath *)indexPath
{
MediaAlbumTableCell *cell = [tableView dequeueReusableCellWithIdentifier:[MediaAlbumTableCell defaultReuseIdentifier] forIndexPath:indexPath];
2016-01-19 17:59:40 +00:00
// Sanity check: cancel pending asynchronous request (if any)
if (cell.tag)
{
2016-01-19 17:59:40 +00:00
[[PHImageManager defaultManager] cancelImageRequest:(PHImageRequestID)cell.tag];
cell.tag = 0;
}
if (indexPath.row < userAlbums.count)
{
PHAssetCollection *collection = userAlbums[indexPath.row];
// Report album title
cell.albumDisplayNameLabel.text = collection.localizedTitle;
// Report album count
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
if ([_mediaTypes indexOfObject:(NSString *)kUTTypeImage] != NSNotFound)
{
2016-01-19 17:59:40 +00:00
if ([_mediaTypes indexOfObject:(NSString *)kUTTypeMovie] != NSNotFound)
{
options.predicate = [NSPredicate predicateWithFormat:@"(mediaType == %d) || (mediaType == %d)", PHAssetMediaTypeImage, PHAssetMediaTypeVideo];
}
else
{
options.predicate = [NSPredicate predicateWithFormat:@"mediaType == %d",PHAssetMediaTypeImage];
}
}
else if ([_mediaTypes indexOfObject:(NSString *)kUTTypeMovie] != NSNotFound)
{
options.predicate = [NSPredicate predicateWithFormat:@"mediaType == %d",PHAssetMediaTypeVideo];
}
PHFetchResult *assets = [PHAsset fetchAssetsInAssetCollection:collection options:options];
cell.albumCountLabel.text = [NSString stringWithFormat:@"%tu", assets.count];
// Report first asset thumbnail (except for 'Recently Deleted' album)
if (assets.count && collection.assetCollectionSubtype != 1000000201)
2016-01-19 17:59:40 +00:00
{
PHAsset *asset = assets[0];
2016-01-19 17:59:40 +00:00
CGSize cellSize = CGSizeMake(73, 73);
2016-01-19 17:59:40 +00:00
PHImageRequestOptions *option = [[PHImageRequestOptions alloc] init];
option.synchronous = NO;
cell.tag = [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:cellSize contentMode:PHImageContentModeAspectFill options:option resultHandler:^(UIImage *result, NSDictionary *info) {
2016-01-19 17:59:40 +00:00
cell.albumThumbnail.contentMode = UIViewContentModeScaleAspectFill;
cell.albumThumbnail.image = result;
cell.tag = 0;
if (collection.assetCollectionSubtype == PHAssetCollectionSubtypeSmartAlbumVideos)
{
cell.bottomLeftIcon.image = [UIImage imageNamed:@"video_icon"];
cell.bottomLeftIcon.hidden = NO;
}
else
{
cell.bottomLeftIcon.hidden = YES;
}
}];
}
else
{
cell.albumThumbnail.image = nil;
cell.albumThumbnail.backgroundColor = [UIColor lightGrayColor];
cell.bottomLeftIcon.hidden = YES;
}
}
2016-01-19 17:59:40 +00:00
return cell;
}
2016-01-19 17:59:40 +00:00
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;
{
2019-01-11 10:45:27 +00:00
cell.backgroundColor = ThemeService.shared.theme.backgroundColor;
// Update the selected background view
2019-01-11 10:45:27 +00:00
if (ThemeService.shared.theme.selectedBackgroundColor)
{
cell.selectedBackgroundView = [[UIView alloc] init];
2019-01-11 10:45:27 +00:00
cell.selectedBackgroundView.backgroundColor = ThemeService.shared.theme.selectedBackgroundColor;
}
else
{
if (tableView.style == UITableViewStylePlain)
{
cell.selectedBackgroundView = nil;
}
else
{
cell.selectedBackgroundView.backgroundColor = nil;
}
}
}
2016-01-19 17:59:40 +00:00
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(nonnull NSIndexPath *)indexPath
{
2016-01-19 17:59:40 +00:00
if (indexPath.row < userAlbums.count)
{
2016-01-19 17:59:40 +00:00
MediaAlbumContentViewController *albumContentViewController = [MediaAlbumContentViewController mediaAlbumContentViewController];
albumContentViewController.mediaTypes = self.mediaTypes;
albumContentViewController.assetsCollection = userAlbums[indexPath.item];
albumContentViewController.delegate = self;
// Enable multiselection only if the delegate is configured to receive them
if ([_delegate respondsToSelector:@selector(mediaPickerController:didSelectAssets:)])
{
albumContentViewController.allowsMultipleSelection = self.allowsMultipleSelection;
}
2016-01-19 17:59:40 +00:00
// Hide back button title
self.navigationItem.backBarButtonItem =[[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil];
[self.navigationController pushViewController:albumContentViewController animated:YES];
}
}
2016-01-19 17:59:40 +00:00
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath
{
2016-01-19 17:59:40 +00:00
// Check whether a asynchronous request is pending
if (cell.tag)
{
2016-01-19 17:59:40 +00:00
[[PHImageManager defaultManager] cancelImageRequest:(PHImageRequestID)cell.tag];
cell.tag = 0;
}
}
2016-01-19 17:59:40 +00:00
#pragma mark - MediaAlbumContentViewControllerDelegate
- (void)mediaAlbumContentViewController:(MediaAlbumContentViewController *)mediaAlbumContentViewController didSelectAsset:(PHAsset*)asset
{
[self didSelectAsset:asset];
}
- (void)mediaAlbumContentViewController:(MediaAlbumContentViewController *)mediaAlbumContentViewController didSelectAssets:(NSArray<PHAsset *> *)assets
{
if ([self.delegate respondsToSelector:@selector(mediaPickerController:didSelectAssets:)])
{
[self.delegate mediaPickerController:self didSelectAssets:assets];
}
}
#pragma mark - Movie player observer
- (void)moviePlayerPlaybackDidFinishNotification:(NSNotification *)notification
{
// Remove player view from superview
[self controlVideoPlayer];
}
2015-08-18 08:04:30 +00:00
@end