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

1064 lines
40 KiB
Objective-C

/*
Copyright 2024 New Vector Ltd.
Copyright 2017 Vector Creations Ltd
Copyright 2015 OpenMarket Ltd
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
#import "MediaPickerViewController.h"
#import "GeneratedInterface-Swift.h"
#import <Photos/Photos.h>
#import <AVKit/AVKit.h>
#import <MobileCoreServices/MobileCoreServices.h>
#import "MediaAlbumContentViewController.h"
#import "MediaAlbumTableCell.h"
@interface MediaPickerViewController () <UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout, MediaAlbumContentViewControllerDelegate>
{
/**
Observe UIApplicationWillEnterForegroundNotification to refresh captures collection when app leaves the background state.
*/
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;
}
@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;
@end
@implementation MediaPickerViewController
#pragma mark - Class methods
+ (instancetype)instantiate
{
return [[[self class] alloc] initWithNibName:NSStringFromClass([MediaPickerViewController class])
bundle:[NSBundle bundleForClass:[MediaPickerViewController class]]];
}
#pragma mark -
- (void)finalizeInit
{
[super finalizeInit];
// 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 = [VectorL10n mediaPickerTitle];
MXWeakify(self);
UIBarButtonItem *closeBarButtonItem = [[MXKBarButtonItem alloc] initWithTitle:[VectorL10n cancel] style:UIBarButtonItemStylePlain action:^{
MXStrongifyAndReturnIfNil(self);
[self.delegate mediaPickerControllerDidCancel:self];
}];
self.navigationItem.rightBarButtonItem = closeBarButtonItem;
// Hide back button title
[self vc_removeBackTitle];
// 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];
// 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);
[self checkPhotoLibraryAuthorizationStatusAndReload];
}];
// 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
{
[ThemeService.shared.theme applyStyleOnNavigationBar:self.navigationController.navigationBar];
self.activityIndicator.backgroundColor = ThemeService.shared.theme.overlayBackgroundColor;
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;
self.userAlbumsTableView.separatorColor = ThemeService.shared.theme.lineBreakColor;
[self setNeedsStatusBarAppearanceUpdate];
}
- (UIStatusBarStyle)preferredStatusBarStyle
{
return ThemeService.shared.theme.statusBarStyle;
}
- (BOOL)prefersStatusBarHidden
{
// Return the current status bar visibility.
return isStatusBarHidden;
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
[self updateRecentCapturesCollectionViewHeightIfNeeded];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self userInterfaceThemeDidChange];
if (!userAlbumsQueue)
{
userAlbumsQueue = dispatch_queue_create("media.picker.user.albums", DISPATCH_QUEUE_SERIAL);
}
[self checkPhotoLibraryAuthorizationStatusAndReload];
}
- (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)checkPhotoLibraryAuthorizationStatusAndReload
{
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
switch (status) {
case PHAuthorizationStatusAuthorized: {
// Load recent captures if this is not already done
dispatch_async(dispatch_get_main_queue(), ^{
[self reloadRecentCapturesCollection];
[self reloadUserLibraryAlbums];
});
break;
}
default:{
dispatch_async(dispatch_get_main_queue(), ^{
[self presentPermissionDeniedAlert];
});
break;
}
}
}];
}
- (void)presentPermissionDeniedAlert
{
NSString *appDisplayName = [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"];
NSString *message = [VectorL10n photoLibraryAccessNotGranted:appDisplayName];
UIAlertController *alert = [UIAlertController alertControllerWithTitle:[VectorL10n mediaPickerTitle]
message:message
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok]
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * action) {
[self.delegate mediaPickerControllerDidCancel:self];
}]];
NSURL *settingsURL = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
[alert addAction:[UIAlertAction actionWithTitle:[VectorL10n 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 checkPhotoLibraryAuthorizationStatusAndReload];
}
}
#pragma mark - UI Refresh/Update
- (void)updateRecentCapturesCollectionViewHeightIfNeeded
{
// 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;
}
}
- (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)
{
// 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];
recentCaptures = [PHAsset fetchAssetsInAssetCollection:assetCollection options:options];
MXLogDebug(@"[MediaPickerVC] lists %tu assets that were recently added to the photo library", recentCaptures.count);
}
else
{
recentCaptures = nil;
}
if (recentCaptures.count)
{
self.recentCapturesCollectionView.hidden = NO;
[self.recentCapturesCollectionView reloadData];
}
else
{
self.recentCapturesCollectionView.hidden = YES;
}
// Force call updateRecentCapturesCollectionViewHeightIfNeeded
[self.recentCapturesCollectionContainerView setNeedsLayout];
[self.recentCapturesCollectionContainerView layoutIfNeeded];
}
- (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];
}
}
else if ([self->_mediaTypes indexOfObject:(NSString *)kUTTypeMovie] != NSNotFound)
{
options.predicate = [NSPredicate predicateWithFormat:@"mediaType == %d",PHAssetMediaTypeVideo];
}
[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];
}
}
}];
// 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;
}
});
});
}
#pragma mark - Validation step
- (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)
{
MXLogDebug(@"[MediaPickerVC] didSelectAsset: Got AVAsset for video");
// Validate first the selected video
[self validateSelectedVideo:asset responseHandler:^(BOOL isValidated) {
if (isValidated)
{
[self.delegate mediaPickerController:self didSelectVideo:asset];
}
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");
}
}
- (void)validateSelectedImage:(UIImage*)selectedImage 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 = YES;
// the user validates the image
[validationView setRightButtonTitle:[VectorL10n 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:[VectorL10n 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:(AVAsset*)selectedVideo 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;
// the user validates the image
[validationView setRightButtonTitle:[VectorL10n 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:[VectorL10n cancel] handler:^(MXKImageView* imageView, NSString* buttonTitle) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
// dismiss the image view
[strongSelf dismissImageValidationView];
handler (NO);
}];
// Display first video frame
videoPlayer = [[AVPlayerViewController alloc] init];
if (videoPlayer)
{
AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:selectedVideo];
videoPlayer.allowsPictureInPicturePlayback = NO;
videoPlayer.updatesNowPlayingInfoCenter = NO;
videoPlayer.player = [AVPlayer playerWithPlayerItem:item];
videoPlayer.videoGravity = AVLayerVideoGravityResizeAspect;
videoPlayer.showsPlaybackControls = NO;
// create a thumbnail for the first frame
AVAssetImageGenerator *generator = [AVAssetImageGenerator assetImageGeneratorWithAsset:selectedVideo];
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:AssetImages.cameraPlay.image forState:UIControlStateNormal];
[videoPlayerControl setImage:AssetImages.cameraPlay.image 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];
}
- (void)dismissImageValidationView
{
if (validationView)
{
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:AssetImages.cameraPlay.image forState:UIControlStateNormal];
[videoPlayerControl setImage:AssetImages.cameraPlay.image 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:AssetImages.cameraStop.image forState:UIControlStateNormal];
[videoPlayerControl setImage:AssetImages.cameraStop.image forState:UIControlStateHighlighted];
[validationView bringSubviewToFront:videoPlayerControl];
}
}
#pragma mark - Action
#pragma mark - UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
// Collection is limited to the first 12 assets
return ((recentCaptures.count > 12) ? 12 : recentCaptures.count);
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
MXKMediaCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[MXKMediaCollectionViewCell defaultReuseIdentifier] forIndexPath:indexPath];
// Sanity check: cancel pending asynchronous request (if any)
if (cell.tag)
{
[[PHImageManager defaultManager] cancelImageRequest:(PHImageRequestID)cell.tag];
cell.tag = 0;
}
if (indexPath.item < recentCaptures.count)
{
PHAsset *asset = recentCaptures[indexPath.item];
CGFloat collectionViewSquareSize = ((collectionView.frame.size.width - 6) / 4); // Here 6 = 3 * cell margin (= 2).
CGSize cellSize = CGSizeMake(collectionViewSquareSize, collectionViewSquareSize);
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) {
cell.mxkImageView.imageView.contentMode = UIViewContentModeScaleAspectFill;
cell.mxkImageView.image = result;
cell.tag = 0;
}];
cell.bottomLeftIcon.image = AssetImages.videoIcon.image;
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
{
if (indexPath.item < recentCaptures.count)
{
[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
{
if (indexPath.item < recentCaptures.count)
{
CGFloat collectionViewSquareSize = ((collectionView.frame.size.width - 6) / 4); // Here 6 = 3 * cell margin (= 2).
CGSize cellSize = CGSizeMake(collectionViewSquareSize, collectionViewSquareSize);
return cellSize;
}
return CGSizeZero;
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return userAlbums.count;
}
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(nonnull NSIndexPath *)indexPath
{
MediaAlbumTableCell *cell = [tableView dequeueReusableCellWithIdentifier:[MediaAlbumTableCell defaultReuseIdentifier] forIndexPath:indexPath];
// Sanity check: cancel pending asynchronous request (if any)
if (cell.tag)
{
[[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)
{
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' and 'Hidden' albums)
BOOL isSensitiveCollection = collection.assetCollectionSubtype == 1000000201 || collection.assetCollectionSubtype == PHAssetCollectionSubtypeSmartAlbumAllHidden;
if (assets.count && !isSensitiveCollection)
{
PHAsset *asset = assets[0];
CGSize cellSize = CGSizeMake(73, 73);
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) {
cell.albumThumbnail.contentMode = UIViewContentModeScaleAspectFill;
cell.albumThumbnail.image = result;
cell.tag = 0;
if (collection.assetCollectionSubtype == PHAssetCollectionSubtypeSmartAlbumVideos)
{
cell.bottomLeftIcon.image = AssetImages.videoIcon.image;
cell.bottomLeftIcon.hidden = NO;
}
else
{
cell.bottomLeftIcon.hidden = YES;
}
}];
}
else
{
cell.albumThumbnail.image = nil;
cell.albumThumbnail.backgroundColor = [UIColor lightGrayColor];
cell.bottomLeftIcon.hidden = YES;
}
}
return cell;
}
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;
{
cell.backgroundColor = ThemeService.shared.theme.backgroundColor;
// Update the selected background view
if (ThemeService.shared.theme.selectedBackgroundColor)
{
cell.selectedBackgroundView = [[UIView alloc] init];
cell.selectedBackgroundView.backgroundColor = ThemeService.shared.theme.selectedBackgroundColor;
}
else
{
if (tableView.style == UITableViewStylePlain)
{
cell.selectedBackgroundView = nil;
}
else
{
cell.selectedBackgroundView.backgroundColor = nil;
}
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(nonnull NSIndexPath *)indexPath
{
if (indexPath.row < userAlbums.count)
{
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;
}
[self.navigationController pushViewController:albumContentViewController animated:YES];
}
}
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath
{
// Check whether a asynchronous request is pending
if (cell.tag)
{
[[PHImageManager defaultManager] cancelImageRequest:(PHImageRequestID)cell.tag];
cell.tag = 0;
}
}
#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];
}
@end