UGCKitAssetsViewController.m 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736
  1. //Copyright (c) 2015 Katsuma Tanaka
  2. //Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to
  3. //deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
  4. //sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
  5. //The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
  6. //THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  7. //FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  8. //LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  9. //IN THE SOFTWARE.
  10. #import "UGCKitAssetsViewController.h"
  11. #import <Photos/Photos.h>
  12. #import "UGCKitAssetLoader.h"
  13. #import "UGCKitAlbumsViewController.h"
  14. #import "UGCKitImageScrollerViewController.h"
  15. // Views
  16. #import "UGCKitMediaPickerViewControllerPrivate.h"
  17. #import "UGCKitAssetCell.h"
  18. #import "UGCKitVideoIndicatorView.h"
  19. #import "UGCKitAssetLoadingController.h"
  20. #import "UGCKitMem.h"
  21. static const CGFloat DefaultSelectionHeight = 110;
  22. static CGSize CGSizeScale(CGSize size, CGFloat scale) {
  23. return CGSizeMake(size.width * scale, size.height * scale);
  24. }
  25. @interface UGCKitMediaPickerViewController (Private)
  26. @property (nonatomic, strong) NSBundle *assetBundle;
  27. @property (nonatomic, weak) UICollectionViewLayout *collectionViewLayout;
  28. @end
  29. @implementation NSIndexSet (Convenience)
  30. - (NSArray *)qb_indexPathsFromIndexesWithSection:(NSUInteger)section
  31. {
  32. NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:self.count];
  33. [self enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
  34. [indexPaths addObject:[NSIndexPath indexPathForItem:idx inSection:section]];
  35. }];
  36. return indexPaths;
  37. }
  38. @end
  39. @implementation UICollectionView (Convenience)
  40. - (NSArray *)qb_indexPathsForElementsInRect:(CGRect)rect
  41. {
  42. NSArray *allLayoutAttributes = [self.collectionViewLayout layoutAttributesForElementsInRect:rect];
  43. if (allLayoutAttributes.count == 0) { return nil; }
  44. NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:allLayoutAttributes.count];
  45. for (UICollectionViewLayoutAttributes *layoutAttributes in allLayoutAttributes) {
  46. NSIndexPath *indexPath = layoutAttributes.indexPath;
  47. [indexPaths addObject:indexPath];
  48. }
  49. return indexPaths;
  50. }
  51. @end
  52. @interface UGCKitAssetsViewController () <PHPhotoLibraryChangeObserver, UICollectionViewDelegateFlowLayout>
  53. @property (nonatomic, strong) UGCKitAssetLoader *loader;
  54. @property (nonatomic, copy) NSArray<AVAsset *> *exportedAssets;
  55. @property (nonatomic, strong) PHFetchResult *fetchResult;
  56. @property (nonatomic, strong) NSArray<PHAssetCollection*> *assetCollections;
  57. @property (nonatomic, strong) PHCachingImageManager *imageManager;
  58. @property (nonatomic, assign) CGRect previousPreheatRect;
  59. @property (nonatomic, assign) BOOL disableScrollToBottom;
  60. @property (nonatomic, strong) NSIndexPath *lastSelectedItemIndexPath;
  61. @property (nonatomic, strong) UGCKitImageScrollerViewController *imagesController;
  62. @end
  63. @implementation UGCKitAssetsViewController
  64. - (void)viewDidLoad
  65. {
  66. [super viewDidLoad];
  67. _loader = [[UGCKitAssetLoader alloc] init];
  68. __weak __typeof(self) wself = self;
  69. _descriptionLabel.textColor = _theme.titleColor;
  70. [_loader load:^{
  71. __strong __typeof(wself) self = wself;
  72. if (nil == self) return;
  73. self->_assetCollections = self->_loader.assetCollections;
  74. [self _updateWithAssetCollection:self->_assetCollections.firstObject];
  75. }];
  76. [self.doneButton setBackgroundImage:_theme.nextIcon forState:UIControlStateNormal];
  77. [self.doneButton setTitle:[_theme localizedString:@"UGCKit.Common.Next"] forState:UIControlStateNormal];
  78. [self.doneButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
  79. UIButton *button = (UIButton *)self.navigationItem.titleView;
  80. [button setTitleColor:_theme.titleColor forState:UIControlStateNormal];
  81. self.view.backgroundColor = _theme.backgroundColor;
  82. self.collectionView.backgroundColor = _theme.backgroundColor;
  83. [self setUpBottomView];
  84. [self resetCachedAssets];
  85. // Register observer
  86. [[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self];
  87. }
  88. - (void)setTheme:(UGCKitTheme *)theme {
  89. _theme = theme;
  90. _descriptionLabel.textColor = _theme.titleColor;
  91. _timeLabel.textColor = _theme.titleColor;
  92. }
  93. - (UICollectionViewLayout *)collectionViewLayout
  94. {
  95. return self.collectionView.collectionViewLayout;
  96. }
  97. - (void)_updateWithAssetCollection:(PHAssetCollection *)assetCollection
  98. {
  99. self.assetCollection = assetCollection;
  100. UIButton *button = (UIButton *)self.navigationItem.titleView;
  101. [button setTitle:assetCollection.localizedTitle
  102. forState:UIControlStateNormal];
  103. button.titleLabel.font = [UIFont systemFontOfSize:17];
  104. [button sizeToFit];
  105. self.navigationItem.titleView = button;
  106. [self.navigationItem.titleView layoutIfNeeded];
  107. [self.collectionView reloadData];
  108. }
  109. - (void)viewWillAppear:(BOOL)animated
  110. {
  111. [super viewWillAppear:animated];
  112. // Configure navigation item
  113. self.navigationItem.title = self.assetCollection.localizedTitle;
  114. self.navigationItem.prompt = self.imagePickerController .config.prompt;
  115. self.navigationController.navigationBar.hidden = NO;
  116. // Configure collection view
  117. self.collectionView.allowsMultipleSelection = self.imagePickerController.allowsMultipleSelection;
  118. // Show/hide 'Done' button
  119. self.doneButton.hidden = !self.imagePickerController.allowsMultipleSelection;
  120. [self updateDoneButtonState];
  121. [self updateSelectionInfo];
  122. [self.collectionView reloadData];
  123. // Scroll to bottom
  124. if (self.fetchResult.count > 0 && self.isMovingToParentViewController && !self.disableScrollToBottom) {
  125. NSIndexPath *indexPath = [NSIndexPath indexPathForItem:(self.fetchResult.count - 1) inSection:0];
  126. [self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionTop animated:NO];
  127. }
  128. }
  129. - (void)viewWillDisappear:(BOOL)animated
  130. {
  131. [super viewWillDisappear:animated];
  132. self.disableScrollToBottom = YES;
  133. }
  134. - (void)viewDidAppear:(BOOL)animated
  135. {
  136. [super viewDidAppear:animated];
  137. self.disableScrollToBottom = NO;
  138. [self updateCachedAssets];
  139. _timeLabel.textColor = _theme.titleColor;
  140. _descriptionLabel.textColor = _theme.titleColor;
  141. }
  142. - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
  143. {
  144. // Save indexPath for the last item
  145. NSIndexPath *indexPath = [[self.collectionView indexPathsForVisibleItems] lastObject];
  146. // Update layout
  147. [self.collectionViewLayout invalidateLayout];
  148. // Restore scroll position
  149. [coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
  150. [self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionBottom animated:NO];
  151. }];
  152. }
  153. - (void)dealloc
  154. {
  155. // Deregister observer
  156. [[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self];
  157. }
  158. #pragma mark - Accessors
  159. - (void)setAssetCollection:(PHAssetCollection *)assetCollection
  160. {
  161. _assetCollection = assetCollection;
  162. [self updateFetchRequest];
  163. [self.collectionView reloadData];
  164. }
  165. - (PHCachingImageManager *)imageManager
  166. {
  167. if (_imageManager == nil) {
  168. _imageManager = [PHCachingImageManager new];
  169. }
  170. return _imageManager;
  171. }
  172. - (BOOL)isAutoDeselectEnabled
  173. {
  174. return (self.imagePickerController.maximumNumberOfSelection == 1
  175. && self.imagePickerController.maximumNumberOfSelection >= self.imagePickerController.minimumNumberOfSelection);
  176. }
  177. #pragma mark - Delegate Methods
  178. - (void)tellDelegateWithAssets:(NSArray<PHAsset *> *)assets {
  179. UGCKitAssetLoadingController *loadvc = [[UGCKitAssetLoadingController alloc] initWithTheme:_theme];
  180. WEAKIFY(self);
  181. WEAKIFY(loadvc);
  182. loadvc.completion = ^(UGCKitResult *result) {
  183. STRONGIFY(self);
  184. self.exportedAssets = weak_loadvc.avAssets;
  185. if (self.imagePickerController.completion) {
  186. self.imagePickerController.completion(result);
  187. }
  188. };
  189. loadvc.combineVideos = self.imagePickerController.config.combineVideos;
  190. UGCKitMediaType mediaType = self.imagePickerController.mediaType;
  191. if (mediaType == UGCKitMediaTypeVideo) {
  192. [loadvc exportAssetList:assets assetType:AssetType_Video];
  193. }else{
  194. [loadvc exportAssetList:assets assetType:AssetType_Image];
  195. }
  196. [self.navigationController pushViewController:loadvc animated:YES];
  197. }
  198. #pragma mark - Actions
  199. - (IBAction)done:(id)sender
  200. {
  201. [self tellDelegateWithAssets:self.imagePickerController.selectedAssets.array];
  202. // if ([self.imagePickerController.delegate respondsToSelector:@selector(qb_imagePickerController:didFinishPickingAssets:)]) {
  203. // [self.imagePickerController.delegate qb_imagePickerController:self.imagePickerController
  204. // didFinishPickingAssets:self.imagePickerController.selectedAssets.array];
  205. // }
  206. }
  207. - (IBAction)cancel:(id)sender
  208. {
  209. if (self.imagePickerController.completion) {
  210. self.imagePickerController.completion([UGCKitResult cancelledResult]);
  211. }
  212. }
  213. #pragma mark - Toolbar
  214. - (void)setUpBottomView
  215. {
  216. self.imagesController = [[UGCKitImageScrollerViewController alloc] initWithImageManage:self.imageManager];
  217. self.imagesController.closeIcon = _theme.closeIcon;
  218. __weak __typeof(self) wself = self;
  219. self.imagesController.onRemoveHandler = ^(NSUInteger index) {
  220. PHAsset *removedAsset = wself.imagePickerController.selectedAssets[index];
  221. NSUInteger indexOfCollection = [wself.fetchResult indexOfObject:removedAsset];
  222. NSMutableOrderedSet *selectedAssets = wself.imagePickerController.selectedAssets;
  223. [selectedAssets removeObjectAtIndex:index];
  224. [wself updateSelectionInfo];
  225. [wself.collectionView deselectItemAtIndexPath:[NSIndexPath indexPathForItem:indexOfCollection inSection:0] animated:YES];
  226. };
  227. self.imagesController.collectionView.backgroundColor = self.view.backgroundColor;
  228. [self.view insertSubview:self.imagesController.view atIndex:0];
  229. CGFloat offset = 0;
  230. if (@available(iOS 11.0, *)) {
  231. offset = [UIApplication sharedApplication].keyWindow.safeAreaInsets.bottom;
  232. }
  233. self.imagesController.view.frame = CGRectMake(0, CGRectGetMaxY(self.view.bounds) - DefaultSelectionHeight - offset,
  234. CGRectGetWidth(self.view.bounds), DefaultSelectionHeight);
  235. self.imagesController.view.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth;
  236. }
  237. - (void)updateSelectionInfo
  238. {
  239. NSMutableOrderedSet *selectedAssets = self.imagePickerController.selectedAssets;
  240. long duration = 0;
  241. for (PHAsset *asset in selectedAssets) {
  242. duration += asset.duration;
  243. }
  244. long minutes = (long)(duration / 60.0);
  245. int seconds = (int)ceil(duration - 60.0 * (double)minutes);
  246. self.timeLabel.text = [NSString stringWithFormat:@"%02ld:%02d", minutes, seconds];
  247. }
  248. #pragma mark - Fetching Assets
  249. - (void)updateFetchRequest
  250. {
  251. if (self.assetCollection) {
  252. PHFetchOptions *options = [PHFetchOptions new];
  253. switch (self.imagePickerController.mediaType) {
  254. case UGCKitMediaTypePhoto:
  255. options.predicate = [NSPredicate predicateWithFormat:@"mediaType == %ld", PHAssetMediaTypeImage];
  256. break;
  257. case UGCKitMediaTypeVideo:
  258. options.predicate = [NSPredicate predicateWithFormat:@"mediaType == %ld", PHAssetMediaTypeVideo];
  259. break;
  260. default:
  261. break;
  262. }
  263. self.fetchResult = [PHAsset fetchAssetsInAssetCollection:self.assetCollection options:options];
  264. if ([self isAutoDeselectEnabled] && self.imagePickerController.selectedAssets.count > 0) {
  265. // Get index of previous selected asset
  266. PHAsset *asset = [self.imagePickerController.selectedAssets firstObject];
  267. NSInteger assetIndex = [self.fetchResult indexOfObject:asset];
  268. self.lastSelectedItemIndexPath = [NSIndexPath indexPathForItem:assetIndex inSection:0];
  269. }
  270. } else {
  271. self.fetchResult = nil;
  272. }
  273. }
  274. #pragma mark - Checking for Selection Limit
  275. - (BOOL)isMinimumSelectionLimitFulfilled
  276. {
  277. return (self.imagePickerController.minimumNumberOfSelection <= self.imagePickerController.selectedAssets.count);
  278. }
  279. - (BOOL)isMaximumSelectionLimitReached
  280. {
  281. NSUInteger minimumNumberOfSelection = MAX(1, self.imagePickerController.minimumNumberOfSelection);
  282. if (minimumNumberOfSelection <= self.imagePickerController.maximumNumberOfSelection) {
  283. return (self.imagePickerController.maximumNumberOfSelection <= self.imagePickerController.selectedAssets.count);
  284. }
  285. return NO;
  286. }
  287. - (void)updateDoneButtonState
  288. {
  289. self.doneButton.enabled = [self isMinimumSelectionLimitFulfilled];
  290. }
  291. #pragma mark - Segue
  292. - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
  293. {
  294. UINavigationController *navigationController = (UINavigationController *)segue.destinationViewController;
  295. UGCKitAlbumsViewController *controller = (UGCKitAlbumsViewController *)navigationController.viewControllers.firstObject;
  296. controller.imagePickerController = self.imagePickerController;
  297. controller.assetCollections = self.assetCollections;
  298. __weak __typeof(self) wself = self;
  299. controller.onPickAssetCollection = ^(PHAssetCollection *collection) {
  300. [self _updateWithAssetCollection:collection];
  301. [wself dismissViewControllerAnimated:YES completion:nil];
  302. };
  303. }
  304. #pragma mark - Asset Caching
  305. - (void)resetCachedAssets
  306. {
  307. [self.imageManager stopCachingImagesForAllAssets];
  308. self.previousPreheatRect = CGRectZero;
  309. }
  310. - (void)updateCachedAssets
  311. {
  312. BOOL isViewVisible = [self isViewLoaded] && self.view.window != nil;
  313. if (!isViewVisible) { return; }
  314. // The preheat window is twice the height of the visible rect
  315. CGRect preheatRect = self.collectionView.bounds;
  316. preheatRect = CGRectInset(preheatRect, 0.0, -0.5 * CGRectGetHeight(preheatRect));
  317. // If scrolled by a "reasonable" amount...
  318. CGFloat delta = ABS(CGRectGetMidY(preheatRect) - CGRectGetMidY(self.previousPreheatRect));
  319. if (delta > CGRectGetHeight(self.collectionView.bounds) / 3.0) {
  320. // Compute the assets to start caching and to stop caching
  321. NSMutableArray *addedIndexPaths = [NSMutableArray array];
  322. NSMutableArray *removedIndexPaths = [NSMutableArray array];
  323. [self computeDifferenceBetweenRect:self.previousPreheatRect andRect:preheatRect addedHandler:^(CGRect addedRect) {
  324. NSArray *indexPaths = [self.collectionView qb_indexPathsForElementsInRect:addedRect];
  325. [addedIndexPaths addObjectsFromArray:indexPaths];
  326. } removedHandler:^(CGRect removedRect) {
  327. NSArray *indexPaths = [self.collectionView qb_indexPathsForElementsInRect:removedRect];
  328. [removedIndexPaths addObjectsFromArray:indexPaths];
  329. }];
  330. NSArray *assetsToStartCaching = [self assetsAtIndexPaths:addedIndexPaths];
  331. NSArray *assetsToStopCaching = [self assetsAtIndexPaths:removedIndexPaths];
  332. CGSize itemSize = [(UICollectionViewFlowLayout *)self.collectionViewLayout itemSize];
  333. CGSize targetSize = CGSizeScale(itemSize, self.traitCollection.displayScale);
  334. [self.imageManager startCachingImagesForAssets:assetsToStartCaching
  335. targetSize:targetSize
  336. contentMode:PHImageContentModeAspectFill
  337. options:nil];
  338. [self.imageManager stopCachingImagesForAssets:assetsToStopCaching
  339. targetSize:targetSize
  340. contentMode:PHImageContentModeAspectFill
  341. options:nil];
  342. self.previousPreheatRect = preheatRect;
  343. }
  344. }
  345. - (void)computeDifferenceBetweenRect:(CGRect)oldRect andRect:(CGRect)newRect addedHandler:(void (^)(CGRect addedRect))addedHandler removedHandler:(void (^)(CGRect removedRect))removedHandler
  346. {
  347. if (CGRectIntersectsRect(newRect, oldRect)) {
  348. CGFloat oldMaxY = CGRectGetMaxY(oldRect);
  349. CGFloat oldMinY = CGRectGetMinY(oldRect);
  350. CGFloat newMaxY = CGRectGetMaxY(newRect);
  351. CGFloat newMinY = CGRectGetMinY(newRect);
  352. if (newMaxY > oldMaxY) {
  353. CGRect rectToAdd = CGRectMake(newRect.origin.x, oldMaxY, newRect.size.width, (newMaxY - oldMaxY));
  354. addedHandler(rectToAdd);
  355. }
  356. if (oldMinY > newMinY) {
  357. CGRect rectToAdd = CGRectMake(newRect.origin.x, newMinY, newRect.size.width, (oldMinY - newMinY));
  358. addedHandler(rectToAdd);
  359. }
  360. if (newMaxY < oldMaxY) {
  361. CGRect rectToRemove = CGRectMake(newRect.origin.x, newMaxY, newRect.size.width, (oldMaxY - newMaxY));
  362. removedHandler(rectToRemove);
  363. }
  364. if (oldMinY < newMinY) {
  365. CGRect rectToRemove = CGRectMake(newRect.origin.x, oldMinY, newRect.size.width, (newMinY - oldMinY));
  366. removedHandler(rectToRemove);
  367. }
  368. } else {
  369. addedHandler(newRect);
  370. removedHandler(oldRect);
  371. }
  372. }
  373. - (NSArray *)assetsAtIndexPaths:(NSArray *)indexPaths
  374. {
  375. if (indexPaths.count == 0) { return nil; }
  376. NSMutableArray *assets = [NSMutableArray arrayWithCapacity:indexPaths.count];
  377. for (NSIndexPath *indexPath in indexPaths) {
  378. if (indexPath.item < self.fetchResult.count) {
  379. PHAsset *asset = self.fetchResult[indexPath.item];
  380. [assets addObject:asset];
  381. }
  382. }
  383. return assets;
  384. }
  385. #pragma mark - PHPhotoLibraryChangeObserver
  386. - (void)photoLibraryDidChange:(PHChange *)changeInstance
  387. {
  388. dispatch_async(dispatch_get_main_queue(), ^{
  389. PHFetchResultChangeDetails *collectionChanges = [changeInstance changeDetailsForFetchResult:self.fetchResult];
  390. if (collectionChanges) {
  391. // Get the new fetch result
  392. self.fetchResult = [collectionChanges fetchResultAfterChanges];
  393. if ([collectionChanges hasMoves]
  394. || [collectionChanges changedIndexes].count
  395. || ![collectionChanges hasIncrementalChanges]) {
  396. // We need to reload all if the incremental diffs are not available
  397. [self.collectionView reloadData];
  398. } else {
  399. // If we have incremental diffs, tell the collection view to animate insertions and deletions
  400. [self.collectionView performBatchUpdates:^{
  401. NSIndexSet *removedIndexes = [collectionChanges removedIndexes];
  402. if ([removedIndexes count]) {
  403. [self.collectionView deleteItemsAtIndexPaths:[removedIndexes qb_indexPathsFromIndexesWithSection:0]];
  404. }
  405. NSIndexSet *insertedIndexes = [collectionChanges insertedIndexes];
  406. if ([insertedIndexes count]) {
  407. [self.collectionView insertItemsAtIndexPaths:[insertedIndexes qb_indexPathsFromIndexesWithSection:0]];
  408. }
  409. } completion:NULL];
  410. }
  411. [self resetCachedAssets];
  412. }
  413. });
  414. }
  415. #pragma mark - UIScrollViewDelegate
  416. - (void)scrollViewDidScroll:(UIScrollView *)scrollView
  417. {
  418. [self updateCachedAssets];
  419. }
  420. #pragma mark - UICollectionViewDataSource
  421. - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
  422. {
  423. return 1;
  424. }
  425. - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
  426. {
  427. return self.fetchResult.count;
  428. }
  429. - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
  430. {
  431. UGCKitAssetCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"AssetCell" forIndexPath:indexPath];
  432. cell.tag = indexPath.item;
  433. cell.selectionColor = self.theme.pickerSelectionBorderColor;
  434. cell.showsOverlayViewWhenSelected = self.imagePickerController.allowsMultipleSelection;
  435. // Image
  436. PHAsset *asset = self.fetchResult[indexPath.item];
  437. CGSize itemSize = [(UICollectionViewFlowLayout *)collectionView.collectionViewLayout itemSize];
  438. CGSize targetSize = CGSizeScale(itemSize, self.traitCollection.displayScale);
  439. [self.imageManager requestImageForAsset:asset
  440. targetSize:targetSize
  441. contentMode:PHImageContentModeAspectFill
  442. options:nil
  443. resultHandler:^(UIImage *result, NSDictionary *info) {
  444. if (cell.tag == indexPath.item) {
  445. cell.imageView.image = result;
  446. }
  447. }];
  448. // Video indicator
  449. if (asset.mediaType == PHAssetMediaTypeVideo) {
  450. cell.videoIndicatorView.hidden = NO;
  451. NSInteger minutes = (NSInteger)(asset.duration / 60.0);
  452. NSInteger seconds = (NSInteger)ceil(asset.duration - 60.0 * (double)minutes);
  453. cell.videoIndicatorView.timeLabel.text = [NSString stringWithFormat:@"%02ld:%02ld", (long)minutes, (long)seconds];
  454. if (asset.mediaSubtypes & PHAssetMediaSubtypeVideoHighFrameRate) {
  455. cell.videoIndicatorView.videoIcon.hidden = YES;
  456. cell.videoIndicatorView.slomoIcon.hidden = NO;
  457. }
  458. else {
  459. cell.videoIndicatorView.videoIcon.hidden = NO;
  460. cell.videoIndicatorView.slomoIcon.hidden = YES;
  461. }
  462. } else {
  463. cell.videoIndicatorView.hidden = YES;
  464. }
  465. // Selection state
  466. if ([self.imagePickerController.selectedAssets containsObject:asset]) {
  467. [cell setSelected:YES];
  468. [collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone];
  469. }
  470. return cell;
  471. }
  472. - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
  473. {
  474. if (kind == UICollectionElementKindSectionFooter) {
  475. UICollectionReusableView *footerView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter
  476. withReuseIdentifier:@"FooterView"
  477. forIndexPath:indexPath];
  478. // Number of assets
  479. UILabel *label = (UILabel *)[footerView viewWithTag:1];
  480. label.textColor = _theme.titleColor;
  481. NSUInteger numberOfPhotos = [self.fetchResult countOfAssetsWithMediaType:PHAssetMediaTypeImage];
  482. NSUInteger numberOfVideos = [self.fetchResult countOfAssetsWithMediaType:PHAssetMediaTypeVideo];
  483. switch (self.imagePickerController.mediaType) {
  484. case UGCKitMediaTypeAny:
  485. {
  486. NSString *format;
  487. if (numberOfPhotos == 1) {
  488. if (numberOfVideos == 1) {
  489. format = [_theme localizedString:@"UGCKit.MediaPicker.Footer.PhotoAndVideo"];
  490. } else {
  491. format = [_theme localizedString:@"UGCKit.MediaPicker.Footer.PhotoAndVideos"];
  492. }
  493. } else if (numberOfVideos == 1) {
  494. format = [_theme localizedString:@"UGCKit.MediaPicker.Footer.PhotosAndVideo"];
  495. } else {
  496. format = [_theme localizedString:@"UGCKit.MediaPicker.Footer.PhotosAndVideos"];
  497. }
  498. label.text = [NSString stringWithFormat:format, numberOfPhotos, numberOfVideos];
  499. }
  500. break;
  501. case UGCKitMediaTypePhoto:
  502. {
  503. NSString *key = (numberOfPhotos == 1) ? @"UGCKit.MediaPicker.Footer.Photo" : @"UGCKit.MediaPicker.Footer.Photos";
  504. NSString *format = [_theme localizedString:key];
  505. label.text = [NSString stringWithFormat:format, numberOfPhotos];
  506. }
  507. break;
  508. case UGCKitMediaTypeVideo:
  509. {
  510. NSString *key = (numberOfVideos == 1) ? @"UGCKit.MediaPicker.Footer.Video" : @"UGCKit.MediaPicker.Footer.Videos";
  511. NSString *format = [_theme localizedString:key];
  512. label.text = [NSString stringWithFormat:format, numberOfVideos];
  513. }
  514. break;
  515. }
  516. return footerView;
  517. }
  518. return [[UICollectionReusableView alloc] init];
  519. }
  520. #pragma mark - UICollectionViewDelegate
  521. - (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath
  522. {
  523. if ([self.imagePickerController.delegate respondsToSelector:@selector(qb_imagePickerController:shouldSelectAsset:)]) {
  524. PHAsset *asset = self.fetchResult[indexPath.item];
  525. return [self.imagePickerController.delegate qb_imagePickerController:self.imagePickerController shouldSelectAsset:asset];
  526. }
  527. if ([self isAutoDeselectEnabled]) {
  528. return YES;
  529. }
  530. return ![self isMaximumSelectionLimitReached];
  531. }
  532. - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
  533. {
  534. UGCKitMediaPickerViewController *imagePickerController = self.imagePickerController;
  535. NSMutableOrderedSet *selectedAssets = imagePickerController.selectedAssets;
  536. PHAsset *asset = self.fetchResult[indexPath.item];
  537. if (imagePickerController.allowsMultipleSelection) {
  538. if ([self isAutoDeselectEnabled] && selectedAssets.count > 0) {
  539. // Remove previous selected asset from set
  540. [selectedAssets removeObjectAtIndex:0];
  541. // Deselect previous selected asset
  542. if (self.lastSelectedItemIndexPath) {
  543. [collectionView deselectItemAtIndexPath:self.lastSelectedItemIndexPath animated:NO];
  544. }
  545. }
  546. // Add asset to set
  547. [selectedAssets addObject:asset];
  548. [self.imagesController addAsset:asset];
  549. self.lastSelectedItemIndexPath = indexPath;
  550. [self updateDoneButtonState];
  551. if (imagePickerController.showsNumberOfSelectedAssets) {
  552. [self updateSelectionInfo];
  553. }
  554. } else {
  555. [self tellDelegateWithAssets:@[asset]];
  556. // if ([imagePickerController.delegate respondsToSelector:@selector(qb_imagePickerController:didFinishPickingAssets:)]) {
  557. // [imagePickerController.delegate qb_imagePickerController:imagePickerController didFinishPickingAssets:@[asset]];
  558. // }
  559. }
  560. if ([imagePickerController.delegate respondsToSelector:@selector(qb_imagePickerController:didSelectAsset:)]) {
  561. [imagePickerController.delegate qb_imagePickerController:imagePickerController didSelectAsset:asset];
  562. }
  563. }
  564. - (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath
  565. {
  566. if (!self.imagePickerController.allowsMultipleSelection) {
  567. return;
  568. }
  569. UGCKitMediaPickerViewController *imagePickerController = self.imagePickerController;
  570. NSMutableOrderedSet *selectedAssets = imagePickerController.selectedAssets;
  571. PHAsset *asset = self.fetchResult[indexPath.item];
  572. // Remove asset from set
  573. NSUInteger index = [selectedAssets indexOfObject:asset];
  574. [selectedAssets removeObject:asset];
  575. if (index != NSNotFound) {
  576. [self.imagesController removeAssetAtIndex:index];
  577. }
  578. self.lastSelectedItemIndexPath = nil;
  579. [self updateDoneButtonState];
  580. if (imagePickerController.showsNumberOfSelectedAssets) {
  581. [self updateSelectionInfo];
  582. }
  583. if ([imagePickerController.delegate respondsToSelector:@selector(qb_imagePickerController:didDeselectAsset:)]) {
  584. [imagePickerController.delegate qb_imagePickerController:imagePickerController didDeselectAsset:asset];
  585. }
  586. }
  587. #pragma mark - UICollectionViewDelegateFlowLayout
  588. - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
  589. {
  590. NSUInteger numberOfColumns;
  591. if (UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation])) {
  592. numberOfColumns = self.imagePickerController.config.numberOfColumnsInPortrait;
  593. } else {
  594. numberOfColumns = self.imagePickerController.config.numberOfColumnsInLandscape;
  595. }
  596. CGFloat width = (CGRectGetWidth(self.view.frame) - 2.0 * (numberOfColumns - 1)) / numberOfColumns;
  597. return CGSizeMake(width, width);
  598. }
  599. @end