From c1ac0e4b3f1e8ca9ff0cdcb6bf5112818ce3a5e8 Mon Sep 17 00:00:00 2001 From: gezihuzi Date: Sun, 16 Aug 2020 23:53:01 +0800 Subject: [PATCH 01/20] Replace assetArray with fetchResult to improve performance --- .../Classes/Assets/AssetsManager+Sync.swift | 6 +- .../Classes/Assets/AssetsManager.swift | 40 +++--- ...etsPhotoViewController+AssetsManager.swift | 13 +- ...AssetsPhotoViewController+Collection.swift | 44 ++++--- .../AssetsPhotoViewController+Delegate.swift | 3 +- .../AssetsPhotoViewController+Model.swift | 16 ++- .../AssetsPhotoViewController+Selection.swift | 9 +- .../AssetsPhotoViewController+UI.swift | 114 ++++++++++++++++-- .../Photo/View/AssetsPhotoLayout.swift | 5 +- 9 files changed, 186 insertions(+), 64 deletions(-) diff --git a/AssetsPickerViewController/Classes/Assets/AssetsManager+Sync.swift b/AssetsPickerViewController/Classes/Assets/AssetsManager+Sync.swift index c47552f..9c0aa73 100644 --- a/AssetsPickerViewController/Classes/Assets/AssetsManager+Sync.swift +++ b/AssetsPickerViewController/Classes/Assets/AssetsManager+Sync.swift @@ -99,7 +99,8 @@ extension AssetsManager: PHPhotoLibraryChangeObserver { let removedIndexes = removedIndexesSet.asArray().sorted(by: { $0.row < $1.row }) var removedAssets = [PHAsset]() for removedIndex in removedIndexes.reversed() { - removedAssets.insert(assetArray.remove(at: removedIndex.row), at: 0) +// removedAssets.insert(assetArray.remove(at: removedIndex.row), at: 0) + // TODO: replace assetArray to fetchResult } // stop caching for removed assets stopCache(assets: removedAssets, size: pickerConfig.assetCacheSize) @@ -112,7 +113,8 @@ extension AssetsManager: PHPhotoLibraryChangeObserver { for insertedIndex in insertedIndexes { let insertedAsset = assetsChangeDetails.fetchResultAfterChanges.object(at: insertedIndex.row) insertedAssets.append(insertedAsset) - assetArray.insert(insertedAsset, at: insertedIndex.row) +// assetArray.insert(insertedAsset, at: insertedIndex.row) + // TODO: replace assetArray to fetchResult } // start caching for inserted assets cache(assets: insertedAssets, size: pickerConfig.assetCacheSize) diff --git a/AssetsPickerViewController/Classes/Assets/AssetsManager.swift b/AssetsPickerViewController/Classes/Assets/AssetsManager.swift index e2f40ec..04ae3e5 100644 --- a/AssetsPickerViewController/Classes/Assets/AssetsManager.swift +++ b/AssetsPickerViewController/Classes/Assets/AssetsManager.swift @@ -37,7 +37,7 @@ open class AssetsManager: NSObject { } } - fileprivate let imageManager = PHCachingImageManager() + let imageManager = PHCachingImageManager() fileprivate var authorizationStatus = PHPhotoLibrary.authorizationStatus() var subscribers = [AssetsManagerDelegate]() @@ -50,7 +50,7 @@ open class AssetsManager: NSObject { var fetchedAlbumsArray = [[PHAssetCollection]]() /// stores sorted array by applying user defined comparator, it's in decreasing order by count by default, and it might same as fetchedAlbumsArray if AssetsPickerConfig has albumFetchOptions without albumComparator var sortedAlbumsArray = [[PHAssetCollection]]() - internal(set) open var assetArray = [PHAsset]() + internal(set) open var fetchResult: PHFetchResult? fileprivate(set) open var defaultAlbum: PHAssetCollection? fileprivate(set) open var cameraRollAlbum: PHAssetCollection! @@ -82,7 +82,7 @@ open class AssetsManager: NSObject { sortedAlbumsArray.removeAll() // clear assets - assetArray.removeAll() + fetchResult = nil // clear fetch results albumsFetchArray.removeAll() @@ -236,8 +236,9 @@ extension AssetsManager { open func image(at index: Int, size: CGSize, isNeedDegraded: Bool = true, completion: @escaping ((UIImage?, Bool) -> Void)) -> PHImageRequestID { let options = PHImageRequestOptions() options.isNetworkAccessAllowed = true + guard let asset = fetchResult?.object(at: index) else { return PHInvalidImageRequestID } return imageManager.requestImage( - for: assetArray[index], + for: asset, targetSize: size, contentMode: .aspectFill, options: options, @@ -318,13 +319,14 @@ extension AssetsManager { return false } self.selectedAlbum = newAlbum - if let fetchResult = fetchMap[newAlbum.localIdentifier] { - let indexSet = IndexSet(0.. Int { - let count = AssetsManager.shared.assetArray.count + let count = AssetsManager.shared.fetchResult?.count ?? 0 updateEmptyView(count: count) return count } @@ -23,11 +23,14 @@ extension AssetsPhotoViewController: UICollectionViewDataSource { logw("Failed to cast UICollectionViewCell.") return cell } - let asset = AssetsManager.shared.assetArray[indexPath.row] - photoCell.asset = asset - photoCell.isVideo = asset.mediaType == .video - if photoCell.isVideo { - photoCell.duration = asset.duration + if let asset = AssetsManager.shared.fetchResult?.object(at: indexPath.row) { + photoCell.asset = asset + photoCell.isVideo = asset.mediaType == .video + if photoCell.isVideo { + photoCell.duration = asset.duration + } + } else { + photoCell.asset = nil } return cell } @@ -40,17 +43,20 @@ extension AssetsPhotoViewController: UICollectionViewDataSource { return } - let asset = AssetsManager.shared.assetArray[indexPath.row] - photoCell.asset = asset - photoCell.isVideo = asset.mediaType == .video - if photoCell.isVideo { - photoCell.duration = asset.duration - } - - if let selectedAsset = selectedMap[asset.localIdentifier] { - if let targetIndex = selectedArray.firstIndex(of: selectedAsset) { - photoCell.count = targetIndex + 1 + if let asset = AssetsManager.shared.fetchResult?.object(at: indexPath.row) { + photoCell.asset = asset + photoCell.isVideo = asset.mediaType == .video + if photoCell.isVideo { + photoCell.duration = asset.duration } + + if let selectedAsset = selectedMap[asset.localIdentifier] { + if let targetIndex = selectedArray.firstIndex(of: selectedAsset) { + photoCell.count = targetIndex + 1 + } + } + } else { + photoCell.asset = nil } fetchService.cancelFetching(at: indexPath) @@ -114,8 +120,10 @@ extension AssetsPhotoViewController: UICollectionViewDataSourcePrefetching { public func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { var assets = [PHAsset]() for indexPath in indexPaths { - if AssetsManager.shared.assetArray.count > indexPath.row { - assets.append(AssetsManager.shared.assetArray[indexPath.row]) + let count = AssetsManager.shared.fetchResult?.count ?? 0 + if count > indexPath.row { + guard let asset = AssetsManager.shared.fetchResult?.object(at: indexPath.row) else { return } + assets.append(asset) } } AssetsManager.shared.cache(assets: assets, size: pickerConfig.assetCacheSize) diff --git a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+Delegate.swift b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+Delegate.swift index 6b1ddb9..a93c533 100644 --- a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+Delegate.swift +++ b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+Delegate.swift @@ -74,7 +74,8 @@ extension AssetsPhotoViewController: UIViewControllerPreviewingDelegate { guard let pressingCell = collectionView.cellForItem(at: pressingIndexPath) else { return nil } previewingContext.sourceRect = pressingCell.frame let previewController = AssetsPreviewController() - previewController.asset = AssetsManager.shared.assetArray[pressingIndexPath.row] + guard let fetchResult = AssetsManager.shared.fetchResult else { return nil } + previewController.asset = fetchResult.object(at: pressingIndexPath.row) return previewController } diff --git a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+Model.swift b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+Model.swift index 4376006..8ca1801 100644 --- a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+Model.swift +++ b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+Model.swift @@ -36,8 +36,10 @@ extension AssetsPhotoViewController { func isSelected(at indexPath: IndexPath) -> Bool { let manager = AssetsManager.shared - guard indexPath.row < manager.assetArray.count else { return false } - if let _ = selectedMap[manager.assetArray[indexPath.row].localIdentifier] { + guard let fetchResult = manager.fetchResult else { return false } + guard indexPath.row < fetchResult.count else { return false } + let asset = fetchResult.object(at: indexPath.row) + if let _ = selectedMap[asset.localIdentifier] { return true } else { return false @@ -47,8 +49,9 @@ extension AssetsPhotoViewController { func select(at indexPath: IndexPath) { defer { logSelectStatus(indexPath, isSelect: true) } let manager = AssetsManager.shared - guard indexPath.row < manager.assetArray.count else { return } - let asset = manager.assetArray[indexPath.row] + guard let fetchResult = manager.fetchResult else { return } + guard indexPath.row < fetchResult.count else { return } + let asset = fetchResult.object(at: indexPath.row) if let _ = selectedMap[asset.localIdentifier] {} else { selectedArray.append(asset) selectedMap[asset.localIdentifier] = asset @@ -61,8 +64,9 @@ extension AssetsPhotoViewController { func deselect(at indexPath: IndexPath) { defer { logSelectStatus(indexPath, isSelect: false) } let manager = AssetsManager.shared - guard indexPath.row < manager.assetArray.count else { return } - let asset = manager.assetArray[indexPath.row] + guard let fetchResult = manager.fetchResult else { return } + guard indexPath.row < fetchResult.count else { return } + let asset = fetchResult.object(at: indexPath.row) guard let targetAsset = selectedMap[asset.localIdentifier] else { logw("Invalid status.") return diff --git a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+Selection.swift b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+Selection.swift index f63f7a5..b1e2a4c 100644 --- a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+Selection.swift +++ b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+Selection.swift @@ -32,7 +32,8 @@ extension AssetsPhotoViewController: UICollectionViewDelegate { if LogConfig.isSelectLogEnabled { logi("shouldSelectItemAt: \(indexPath.row)") } if let delegate = self.delegate { - let shouldSelect = delegate.assetsPicker?(controller: picker, shouldSelect: AssetsManager.shared.assetArray[indexPath.row], at: indexPath) ?? true + guard let fetchResult = AssetsManager.shared.fetchResult else { return false } + let shouldSelect = delegate.assetsPicker?(controller: picker, shouldSelect: fetchResult.object(at: indexPath.row), at: indexPath) ?? true guard shouldSelect else { return false } } @@ -62,7 +63,8 @@ extension AssetsPhotoViewController: UICollectionViewDelegate { public func collectionView(_ collectionView: UICollectionView, shouldDeselectItemAt indexPath: IndexPath) -> Bool { if LogConfig.isSelectLogEnabled { logi("shouldDeselectItemAt: \(indexPath.row)") } if let delegate = self.delegate { - let shouldDeselect = delegate.assetsPicker?(controller: picker, shouldDeselect: AssetsManager.shared.assetArray[indexPath.row], at: indexPath) ?? true + guard let fetchResult = AssetsManager.shared.fetchResult else { return false } + let shouldDeselect = delegate.assetsPicker?(controller: picker, shouldDeselect: fetchResult.object(at: indexPath.row), at: indexPath) ?? true guard shouldDeselect else { return false } } deselect(at: indexPath) @@ -86,7 +88,8 @@ extension AssetsPhotoViewController { return } for selectedIndexPath in indexPathsForSelectedItems { - if let _ = selectedMap[AssetsManager.shared.assetArray[selectedIndexPath.row].localIdentifier] { + guard let fetchResult = AssetsManager.shared.fetchResult else { return } + if let _ = selectedMap[fetchResult.object(at: selectedIndexPath.row).localIdentifier] { } else { loge("selected item not found in local map!") diff --git a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+UI.swift b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+UI.swift index d7b8e95..97d2bba 100644 --- a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+UI.swift +++ b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+UI.swift @@ -57,7 +57,9 @@ extension AssetsPhotoViewController { } func deselectOldestIfNeeded(isForced: Bool = false) { - if selectedArray.count > pickerConfig.assetsMaximumSelectionCount, let firstSelectedAsset = selectedArray.first, let indexToDeselect = AssetsManager.shared.assetArray.firstIndex(of: firstSelectedAsset) { + guard let fetchResult = AssetsManager.shared.fetchResult else { return } + if selectedArray.count > pickerConfig.assetsMaximumSelectionCount, let firstSelectedAsset = selectedArray.first { + let indexToDeselect = fetchResult.index(of: firstSelectedAsset) let indexPathToDeselect = IndexPath(row: indexToDeselect, section: 0) deselect(at: indexPathToDeselect) deselectCell(at: indexPathToDeselect, isForced: isForced) @@ -90,10 +92,10 @@ extension AssetsPhotoViewController { func scrollToLastItemIfNeeded() { - let assets = AssetsManager.shared.assetArray - guard !assets.isEmpty else { return } + guard let fetchResult = AssetsManager.shared.fetchResult else { return } + guard !(fetchResult.count == 0) else { return } if pickerConfig.assetsIsScrollToBottom == true { - self.collectionView.scrollToItem(at: IndexPath(row: assets.count - 1, section: 0), at: .bottom, animated: false) + self.collectionView.scrollToItem(at: IndexPath(row: fetchResult.count - 1, section: 0), at: .bottom, animated: false) } else { self.collectionView.scrollToItem(at: IndexPath(row: 0, section: 0), at: .bottom, animated: false) } @@ -117,13 +119,13 @@ extension AssetsPhotoViewController { func updateSelectionCount() { let visibleIndexPaths = collectionView.indexPathsForVisibleItems - let assets = AssetsManager.shared.assetArray + guard let fetchResult = AssetsManager.shared.fetchResult else { return } for visibleIndexPath in visibleIndexPaths { - guard assets.count > visibleIndexPath.row else { - loge("Referred wrong index\(visibleIndexPath.row) while asset count is \(assets.count).") + guard fetchResult.count > visibleIndexPath.row else { + loge("Referred wrong index\(visibleIndexPath.row) while asset count is \(fetchResult.count).") break } - if let selectedAsset = selectedMap[assets[visibleIndexPath.row].localIdentifier], var photoCell = collectionView.cellForItem(at: visibleIndexPath) as? AssetsPhotoCellProtocol { + if let selectedAsset = selectedMap[fetchResult.object(at: visibleIndexPath.row).localIdentifier], var photoCell = collectionView.cellForItem(at: visibleIndexPath) as? AssetsPhotoCellProtocol { if let selectedIndex = selectedArray.firstIndex(of: selectedAsset) { photoCell.count = selectedIndex + 1 } @@ -196,4 +198,100 @@ extension AssetsPhotoViewController { } return titleString } + + func updateCachedAssets() { + let isViewVisible = isViewLoaded && view.window != nil + + if !isViewVisible { + return + } + + let bounds = collectionView.bounds + + // The preheat window is twice the height of the visible rect + var preheatRect = bounds + preheatRect = preheatRect.insetBy(dx: 0.0, dy: -0.5 * preheatRect.height) + + // If scrolled by a "reasonable" amount... + let delta = abs(preheatRect.midY - previousPreheatRect.midY) + + if delta > (bounds.height / 3.0) { + var addedIndexPaths: [IndexPath] = [] + var removedIndexPaths: [IndexPath] = [] + + computeDifferenceBetweenRect(previousPreheatRect, newRect: preheatRect, add: { (rect) in + let indexPaths = getIndexPathsForElements(in: rect) + addedIndexPaths.append(contentsOf: indexPaths) + }) { (rect) in + let indexPaths = getIndexPathsForElements(in: rect) + removedIndexPaths.append(contentsOf: indexPaths) + } + + let assetsToStartCaching = getAssets(at: addedIndexPaths) + let assetsToStopCaching = getAssets(at: removedIndexPaths) + + let isPortrait = UIApplication.shared.statusBarOrientation.isPortrait + let itemSize = isPortrait ? pickerConfig.albumPortraitCellSize : pickerConfig.albumLandscapeCellSize + let scale = traitCollection.displayScale + let targetSize = CGSize(width: itemSize.width * scale, height: itemSize.height * scale) + AssetsManager.shared.imageManager.startCachingImages(for: assetsToStartCaching, targetSize: targetSize, contentMode: .aspectFill, options: nil) + AssetsManager.shared.imageManager.stopCachingImages(for: assetsToStopCaching, targetSize: targetSize, contentMode: .aspectFill, options: nil) + self.previousPreheatRect = preheatRect + } + } + + func getIndexPathsForElements(in rect: CGRect) -> [IndexPath] { + let allLayoutAttributes = collectionView.collectionViewLayout.layoutAttributesForElements(in: rect) + guard let attributes = allLayoutAttributes else { return [] } + guard attributes.count == 0 else { return [] } + var indexPaths: [IndexPath] = [] + for attribut in attributes { + indexPaths.append(attribut.indexPath) + } + return indexPaths + } + + func getAssets(at indexPaths: [IndexPath]) -> [PHAsset] { + if indexPaths.count == 0 { return [] } + guard let fetchResult = AssetsManager.shared.fetchResult else { return [] } + var asstes: [PHAsset] = [] + for indexPath in indexPaths { + if indexPath.item < fetchResult.count && indexPath.item != 0 { + let index = fetchResult.count - indexPath.item + let asset = fetchResult.object(at: index) + asstes.append(asset) + } + } + return asstes + } + + func computeDifferenceBetweenRect(_ oldRect: CGRect, newRect: CGRect, add: (CGRect) -> Void, remove: (CGRect) -> Void) { + if newRect.intersects(oldRect) { + let oldMaxY = oldRect.maxY + let oldMinY = oldRect.minY + let newMaxY = newRect.maxY + let newMinY = newRect.minY + + if newMaxY > oldMaxY { + let rect = CGRect(x: newRect.origin.x, y: oldMaxY, width: newRect.size.width, height: (newMaxY - oldMaxY)) + add(rect) + } + if oldMinY > newMinY { + let rect = CGRect(x: newRect.origin.x, y: newMinY, width: newRect.size.width, height: oldMinY - newMinY) + add(rect) + } + if newMaxY < oldMaxY { + let rect = CGRect(x: newRect.origin.x, y: newMaxY, width: newRect.size.width, height: oldMaxY - newMaxY) + remove(rect) + } + if oldMinY < newMinY { + let rect = CGRect(x: newRect.origin.x, y: oldMinY, width: newRect.size.width, height: newMinY - oldMinY) + remove(rect) + } + + } else { + add(newRect) + remove(oldRect) + } + } } diff --git a/AssetsPickerViewController/Classes/Photo/View/AssetsPhotoLayout.swift b/AssetsPickerViewController/Classes/Photo/View/AssetsPhotoLayout.swift index f6f63f8..3555513 100644 --- a/AssetsPickerViewController/Classes/Photo/View/AssetsPhotoLayout.swift +++ b/AssetsPickerViewController/Classes/Photo/View/AssetsPhotoLayout.swift @@ -39,8 +39,9 @@ open class AssetsPhotoLayout: UICollectionViewFlowLayout { extension AssetsPhotoLayout { open func expectedContentHeight(forViewSize size: CGSize, isPortrait: Bool) -> CGFloat { - var rows = AssetsManager.shared.assetArray.count / (isPortrait ? pickerConfig.assetPortraitColumnCount : pickerConfig.assetLandscapeColumnCount) - let remainder = AssetsManager.shared.assetArray.count % (isPortrait ? pickerConfig.assetPortraitColumnCount : pickerConfig.assetLandscapeColumnCount) + guard let fetchResult = AssetsManager.shared.fetchResult else { return 0.0 } + var rows = fetchResult.count / (isPortrait ? pickerConfig.assetPortraitColumnCount : pickerConfig.assetLandscapeColumnCount) + let remainder = fetchResult.count % (isPortrait ? pickerConfig.assetPortraitColumnCount : pickerConfig.assetLandscapeColumnCount) rows += remainder > 0 ? 1 : 0 let cellSize = isPortrait ? pickerConfig.assetPortraitCellSize(forViewSize: UIScreen.main.portraitContentSize) : pickerConfig.assetLandscapeCellSize(forViewSize: UIScreen.main.landscapeContentSize) From 7fe2f091b2f4c83f511f660b4183b2cefd28f6b6 Mon Sep 17 00:00:00 2001 From: gezihuzi Date: Sun, 16 Aug 2020 23:53:43 +0800 Subject: [PATCH 02/20] Added cache for updating pictures when scrolling --- .../AssetsPhotoViewController.swift | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController.swift b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController.swift index b2848e4..4be80b9 100644 --- a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController.swift +++ b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController.swift @@ -30,6 +30,8 @@ open class AssetsPhotoViewController: UIViewController { let fetchService = AssetsFetchService() + var previousPreheatRect: CGRect = .zero + lazy var cancelButtonItem: UIBarButtonItem = { let buttonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, @@ -180,7 +182,8 @@ open class AssetsPhotoViewController: UIViewController { super.viewDidLayoutSubviews() if !didSetInitialPosition { if pickerConfig.assetsIsScrollToBottom { - let count = AssetsManager.shared.assetArray.count + guard let fetchResult = AssetsManager.shared.fetchResult else { return } + let count = fetchResult.count if count > 0 { if self.collectionView.collectionViewLayout.collectionViewContentSize.height > 0 { let lastRow = self.collectionView.numberOfItems(inSection: 0) - 1 @@ -194,13 +197,13 @@ open class AssetsPhotoViewController: UIViewController { open func deselectAll() { var indexPaths = [IndexPath]() + guard let fetchResult = AssetsManager.shared.fetchResult else { return } for selectedAsset in selectedArray { - if let row = AssetsManager.shared.assetArray.firstIndex(of: selectedAsset) { - let indexPath = IndexPath(row: row, section: 0) - deselectCell(at: indexPath) - delegate?.assetsPicker?(controller: picker, didDeselect: selectedAsset, at: indexPath) - indexPaths.append(indexPath) - } + let row = fetchResult.index(of: selectedAsset) + let indexPath = IndexPath(row: row, section: 0) + deselectCell(at: indexPath) + delegate?.assetsPicker?(controller: picker, didDeselect: selectedAsset, at: indexPath) + indexPaths.append(indexPath) } updateSelectionCount() updateNavigationStatus() @@ -244,6 +247,7 @@ open class AssetsPhotoViewController: UIViewController { if traitCollection.forceTouchCapability == .available { previewing = registerForPreviewing(with: self, sourceView: collectionView) } + updateCachedAssets() } override open func viewDidDisappear(_ animated: Bool) { @@ -258,6 +262,10 @@ open class AssetsPhotoViewController: UIViewController { deinit { logd("Released \(type(of: self))") } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + updateCachedAssets() + } } extension UICollectionView { From d2027c515bf1aa3ef422569913f27a85cae06dce Mon Sep 17 00:00:00 2001 From: gezihuzi Date: Mon, 17 Aug 2020 00:32:58 +0800 Subject: [PATCH 03/20] Replace photos with fetchResult --- .../Classes/Assets/AssetsManager.swift | 25 +++++++++++-------- .../AssetsPhotoViewController+Setup.swift | 7 +++--- .../AssetsPhotoViewController+UI.swift | 22 ++++++++-------- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/AssetsPickerViewController/Classes/Assets/AssetsManager.swift b/AssetsPickerViewController/Classes/Assets/AssetsManager.swift index 04ae3e5..73a8356 100644 --- a/AssetsPickerViewController/Classes/Assets/AssetsManager.swift +++ b/AssetsPickerViewController/Classes/Assets/AssetsManager.swift @@ -57,7 +57,7 @@ open class AssetsManager: NSObject { fileprivate(set) open var selectedAlbum: PHAssetCollection? fileprivate var isFetchedAlbums: Bool = false - fileprivate var resourceLoadingQueue: DispatchQueue = DispatchQueue(label: "com.assetspicker.loader", qos: .userInitiated) + fileprivate var resourceLoadingQueue: DispatchQueue = DispatchQueue(label: "com.assetspicker.loader", qos: .default) private override init() { super.init() @@ -343,20 +343,20 @@ extension AssetsManager { } } - open func selectAsync(album newAlbum: PHAssetCollection, completion: @escaping (Bool, [PHAsset]) -> Void) { + open func selectAsync(album newAlbum: PHAssetCollection, completion: @escaping (Bool, PHFetchResult?) -> Void) { if let oldAlbumIdentifier = self.selectedAlbum?.localIdentifier, oldAlbumIdentifier == newAlbum.localIdentifier { logi("Selected same album.") - completion(false, []) + completion(false, nil) } self.selectedAlbum = newAlbum - guard let album = AssetsManager.shared.selectedAlbum else { completion(false, []) + guard let album = AssetsManager.shared.selectedAlbum else { completion(false, nil) return } - guard let fetchResult = AssetsManager.shared.fetchMap[album.localIdentifier] else { completion(false, []) + guard let fetchResult = AssetsManager.shared.fetchMap[album.localIdentifier] else { completion(false, nil) return } self.fetchResult = fetchResult - completion(true, []) + completion(true, fetchResult) } } @@ -513,7 +513,7 @@ extension AssetsManager { } } - open func fetchAssets(isRefetch: Bool = false, completion: (([PHAsset]) -> Void)? = nil) { + open func fetchAssets(isRefetch: Bool = false, completion: ((PHFetchResult?) -> Void)? = nil) { fetchAlbums(isRefetch: isRefetch, completion: { [weak self] _ in @@ -523,8 +523,8 @@ extension AssetsManager { } // set default album - self.selectAsync(album: self.defaultAlbum ?? self.cameraRollAlbum) { result, photos in - completion?(photos) + self.selectAsync(album: self.defaultAlbum ?? self.cameraRollAlbum) { successful, result in + completion?(result) } }) @@ -536,7 +536,10 @@ extension AssetsManager { let albumFetchResult = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: fetchOption) var fetchedAlbums = [PHAssetCollection]() - albumFetchResult.enumerateObjects({ (album, _, _) in + let indexSet = IndexSet(integersIn: 0..) { if selectedArray.count > 0 { // initialize preselected assets selectedArray.forEach({ [weak self] (asset) in - if let row = photos.firstIndex(of: asset) { - let indexPathToSelect = IndexPath(row: row, section: 0) - let scrollPosition = UICollectionView.ScrollPosition(rawValue: 0) - self?.collectionView.selectItem(at: indexPathToSelect, - animated: false, - scrollPosition: scrollPosition) - } + let row = result.index(of: asset) + let indexPathToSelect = IndexPath(row: row, section: 0) + let scrollPosition = UICollectionView.ScrollPosition(rawValue: 0) + self?.collectionView.selectItem(at: indexPathToSelect, + animated: false, + scrollPosition: scrollPosition) }) updateSelectionCount() } From 4c178919f7028583581d368d94f297139085f51c Mon Sep 17 00:00:00 2001 From: gezihuzi Date: Wed, 19 Aug 2020 10:39:43 +0800 Subject: [PATCH 04/20] Improve resource caching --- .../Classes/Assets/AssetsManager+Sync.swift | 4 ++-- .../Classes/Assets/AssetsManager.swift | 2 +- .../Photo/Controller/AssetsPhotoViewController+UI.swift | 9 +++------ 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/AssetsPickerViewController/Classes/Assets/AssetsManager+Sync.swift b/AssetsPickerViewController/Classes/Assets/AssetsManager+Sync.swift index 9c0aa73..21d5ae5 100644 --- a/AssetsPickerViewController/Classes/Assets/AssetsManager+Sync.swift +++ b/AssetsPickerViewController/Classes/Assets/AssetsManager+Sync.swift @@ -99,8 +99,8 @@ extension AssetsManager: PHPhotoLibraryChangeObserver { let removedIndexes = removedIndexesSet.asArray().sorted(by: { $0.row < $1.row }) var removedAssets = [PHAsset]() for removedIndex in removedIndexes.reversed() { -// removedAssets.insert(assetArray.remove(at: removedIndex.row), at: 0) - // TODO: replace assetArray to fetchResult + let asset = fetchResult.object(at: removedIndex.row) + removedAssets.insert(asset, at: 0) } // stop caching for removed assets stopCache(assets: removedAssets, size: pickerConfig.assetCacheSize) diff --git a/AssetsPickerViewController/Classes/Assets/AssetsManager.swift b/AssetsPickerViewController/Classes/Assets/AssetsManager.swift index 73a8356..bc1512c 100644 --- a/AssetsPickerViewController/Classes/Assets/AssetsManager.swift +++ b/AssetsPickerViewController/Classes/Assets/AssetsManager.swift @@ -37,7 +37,7 @@ open class AssetsManager: NSObject { } } - let imageManager = PHCachingImageManager() + fileprivate let imageManager = PHCachingImageManager() fileprivate var authorizationStatus = PHPhotoLibrary.authorizationStatus() var subscribers = [AssetsManagerDelegate]() diff --git a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+UI.swift b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+UI.swift index dbc749a..ef7b94e 100644 --- a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+UI.swift +++ b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+UI.swift @@ -230,12 +230,9 @@ extension AssetsPhotoViewController { let assetsToStartCaching = getAssets(at: addedIndexPaths) let assetsToStopCaching = getAssets(at: removedIndexPaths) - let isPortrait = UIApplication.shared.statusBarOrientation.isPortrait - let itemSize = isPortrait ? pickerConfig.albumPortraitCellSize : pickerConfig.albumLandscapeCellSize - let scale = traitCollection.displayScale - let targetSize = CGSize(width: itemSize.width * scale, height: itemSize.height * scale) - AssetsManager.shared.imageManager.startCachingImages(for: assetsToStartCaching, targetSize: targetSize, contentMode: .aspectFill, options: nil) - AssetsManager.shared.imageManager.stopCachingImages(for: assetsToStopCaching, targetSize: targetSize, contentMode: .aspectFill, options: nil) + let targetSize = pickerConfig.assetCacheSize + AssetsManager.shared.cache(assets: assetsToStartCaching, size: targetSize) + AssetsManager.shared.stopCache(assets: assetsToStopCaching, size: targetSize) self.previousPreheatRect = preheatRect } } From 0c39f1f1bb626c2a2656de998732aaf0204aaae3 Mon Sep 17 00:00:00 2001 From: gezihuzi Date: Wed, 19 Aug 2020 10:57:08 +0800 Subject: [PATCH 05/20] Improve the view update when the resource library changes --- .../Classes/Assets/AssetsManager+Sync.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/AssetsPickerViewController/Classes/Assets/AssetsManager+Sync.swift b/AssetsPickerViewController/Classes/Assets/AssetsManager+Sync.swift index 21d5ae5..214b750 100644 --- a/AssetsPickerViewController/Classes/Assets/AssetsManager+Sync.swift +++ b/AssetsPickerViewController/Classes/Assets/AssetsManager+Sync.swift @@ -98,10 +98,13 @@ extension AssetsManager: PHPhotoLibraryChangeObserver { if let removedIndexesSet = assetsChangeDetails.removedIndexes { let removedIndexes = removedIndexesSet.asArray().sorted(by: { $0.row < $1.row }) var removedAssets = [PHAsset]() + let result = assetsChangeDetails.fetchResultAfterChanges for removedIndex in removedIndexes.reversed() { let asset = fetchResult.object(at: removedIndex.row) removedAssets.insert(asset, at: 0) } + // update date source + self.fetchResult = result // stop caching for removed assets stopCache(assets: removedAssets, size: pickerConfig.assetCacheSize) notifySubscribers({ $0.assetsManager(manager: self, removedAssets: removedAssets, at: removedIndexes) }, condition: removedAssets.count > 0) @@ -110,12 +113,13 @@ extension AssetsManager: PHPhotoLibraryChangeObserver { if let insertedIndexesSet = assetsChangeDetails.insertedIndexes { let insertedIndexes = insertedIndexesSet.asArray().sorted(by: { $0.row < $1.row }) var insertedAssets = [PHAsset]() + let result = assetsChangeDetails.fetchResultAfterChanges for insertedIndex in insertedIndexes { - let insertedAsset = assetsChangeDetails.fetchResultAfterChanges.object(at: insertedIndex.row) + let insertedAsset = result.object(at: insertedIndex.row) insertedAssets.append(insertedAsset) -// assetArray.insert(insertedAsset, at: insertedIndex.row) - // TODO: replace assetArray to fetchResult } + // update date source + self.fetchResult = result // start caching for inserted assets cache(assets: insertedAssets, size: pickerConfig.assetCacheSize) notifySubscribers({ $0.assetsManager(manager: self, insertedAssets: insertedAssets, at: insertedIndexes) }, condition: insertedAssets.count > 0) From e95392d4f26dbc46e1f295431730348f9a176e36 Mon Sep 17 00:00:00 2001 From: gezihuzi Date: Wed, 19 Aug 2020 13:43:51 +0800 Subject: [PATCH 06/20] update qos --- AssetsPickerViewController/Classes/Assets/AssetsManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AssetsPickerViewController/Classes/Assets/AssetsManager.swift b/AssetsPickerViewController/Classes/Assets/AssetsManager.swift index bc1512c..bd19833 100644 --- a/AssetsPickerViewController/Classes/Assets/AssetsManager.swift +++ b/AssetsPickerViewController/Classes/Assets/AssetsManager.swift @@ -57,7 +57,7 @@ open class AssetsManager: NSObject { fileprivate(set) open var selectedAlbum: PHAssetCollection? fileprivate var isFetchedAlbums: Bool = false - fileprivate var resourceLoadingQueue: DispatchQueue = DispatchQueue(label: "com.assetspicker.loader", qos: .default) + fileprivate var resourceLoadingQueue: DispatchQueue = DispatchQueue(label: "com.assetspicker.loader", qos: .userInitiated) private override init() { super.init() From c3ed357a55e5fbf709168e89f44684a500c102ac Mon Sep 17 00:00:00 2001 From: gezihuzi Date: Thu, 20 Aug 2020 21:26:17 +0800 Subject: [PATCH 07/20] Improved performance when scrolling views --- .../AssetsPhotoViewController+Delegate.swift | 9 +++++---- .../AssetsPhotoViewController+Setup.swift | 1 + .../AssetsPhotoViewController+UI.swift | 20 +++++++++---------- .../AssetsPhotoViewController.swift | 5 ----- 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+Delegate.swift b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+Delegate.swift index a93c533..29fd908 100644 --- a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+Delegate.swift +++ b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+Delegate.swift @@ -45,11 +45,12 @@ extension AssetsPhotoViewController: UIGestureRecognizerDelegate { } // MARK: - UIScrollViewDelegate -//extension AssetsPhotoViewController: UIScrollViewDelegate { -// public func scrollViewDidScroll(_ scrollView: UIScrollView) { +extension AssetsPhotoViewController: UIScrollViewDelegate { + public func scrollViewDidScroll(_ scrollView: UIScrollView) { // logi("contentOffset: \(scrollView.contentOffset)") -// } -//} + updateCachedAssets() + } +} // MARK: - AssetsAlbumViewControllerDelegate extension AssetsPhotoViewController: AssetsAlbumViewControllerDelegate { diff --git a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+Setup.swift b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+Setup.swift index 3556d9a..30cee68 100644 --- a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+Setup.swift +++ b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+Setup.swift @@ -81,6 +81,7 @@ extension AssetsPhotoViewController { self.collectionView.reloadData() self.preselectItemsIfNeeded(result: fetchResult) self.scrollToLastItemIfNeeded() + self.updateCachedAssets(force: true) // hide loading self.loadingPlaceholderView.isHidden = true self.loadingActivityIndicatorView.stopAnimating() diff --git a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+UI.swift b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+UI.swift index ef7b94e..686ab32 100644 --- a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+UI.swift +++ b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+UI.swift @@ -199,7 +199,7 @@ extension AssetsPhotoViewController { return titleString } - func updateCachedAssets() { + func updateCachedAssets(force: Bool = false) { let isViewVisible = isViewLoaded && view.window != nil if !isViewVisible { @@ -215,11 +215,11 @@ extension AssetsPhotoViewController { // If scrolled by a "reasonable" amount... let delta = abs(preheatRect.midY - previousPreheatRect.midY) - if delta > (bounds.height / 3.0) { + if (delta > (bounds.height / 3.0)) || force { var addedIndexPaths: [IndexPath] = [] var removedIndexPaths: [IndexPath] = [] - computeDifferenceBetweenRect(previousPreheatRect, newRect: preheatRect, add: { (rect) in + computeDifferenceBetweenRect(previousPreheatRect, newRect: preheatRect, added: { (rect) in let indexPaths = getIndexPathsForElements(in: rect) addedIndexPaths.append(contentsOf: indexPaths) }) { (rect) in @@ -262,7 +262,7 @@ extension AssetsPhotoViewController { return asstes } - func computeDifferenceBetweenRect(_ oldRect: CGRect, newRect: CGRect, add: (CGRect) -> Void, remove: (CGRect) -> Void) { + func computeDifferenceBetweenRect(_ oldRect: CGRect, newRect: CGRect, added: (CGRect) -> Void, removed: (CGRect) -> Void) { if newRect.intersects(oldRect) { let oldMaxY = oldRect.maxY let oldMinY = oldRect.minY @@ -271,24 +271,24 @@ extension AssetsPhotoViewController { if newMaxY > oldMaxY { let rect = CGRect(x: newRect.origin.x, y: oldMaxY, width: newRect.size.width, height: (newMaxY - oldMaxY)) - add(rect) + added(rect) } if oldMinY > newMinY { let rect = CGRect(x: newRect.origin.x, y: newMinY, width: newRect.size.width, height: oldMinY - newMinY) - add(rect) + added(rect) } if newMaxY < oldMaxY { let rect = CGRect(x: newRect.origin.x, y: newMaxY, width: newRect.size.width, height: oldMaxY - newMaxY) - remove(rect) + removed(rect) } if oldMinY < newMinY { let rect = CGRect(x: newRect.origin.x, y: oldMinY, width: newRect.size.width, height: newMinY - oldMinY) - remove(rect) + removed(rect) } } else { - add(newRect) - remove(oldRect) + added(newRect) + removed(oldRect) } } } diff --git a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController.swift b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController.swift index 4be80b9..489ae42 100644 --- a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController.swift +++ b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController.swift @@ -247,7 +247,6 @@ open class AssetsPhotoViewController: UIViewController { if traitCollection.forceTouchCapability == .available { previewing = registerForPreviewing(with: self, sourceView: collectionView) } - updateCachedAssets() } override open func viewDidDisappear(_ animated: Bool) { @@ -262,10 +261,6 @@ open class AssetsPhotoViewController: UIViewController { deinit { logd("Released \(type(of: self))") } - - public func scrollViewDidScroll(_ scrollView: UIScrollView) { - updateCachedAssets() - } } extension UICollectionView { From fd845d19a70fe1a5b0fc709115af0d34d0e8db3d Mon Sep 17 00:00:00 2001 From: gezihuzi Date: Sat, 22 Aug 2020 19:03:09 +0800 Subject: [PATCH 08/20] Album performance optimization Modify asynchronous fetching of assets within the album list. --- .../Classes/Assets/AssetsManager.swift | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/AssetsPickerViewController/Classes/Assets/AssetsManager.swift b/AssetsPickerViewController/Classes/Assets/AssetsManager.swift index bd19833..b9d27f2 100644 --- a/AssetsPickerViewController/Classes/Assets/AssetsManager.swift +++ b/AssetsPickerViewController/Classes/Assets/AssetsManager.swift @@ -58,6 +58,7 @@ open class AssetsManager: NSObject { fileprivate var isFetchedAlbums: Bool = false fileprivate var resourceLoadingQueue: DispatchQueue = DispatchQueue(label: "com.assetspicker.loader", qos: .userInitiated) + fileprivate var albumLoadingQueue: DispatchQueue = DispatchQueue(label: "com.assetspicker.album.loader", qos: .default) private override init() { super.init() @@ -540,20 +541,35 @@ extension AssetsManager { let albums = albumFetchResult.objects(at: indexSet) for album in albums { - // fetch assets - self.fetchAlbum(album: album) // set default album if album.assetCollectionSubtype == self.pickerConfig.albumDefaultType { self.defaultAlbum = album + + // fetch assets + self.fetchAlbum(album: album) } // save alternative album if album.assetCollectionSubtype == .smartAlbumUserLibrary { self.cameraRollAlbum = album + + // fetch assets + self.fetchAlbum(album: album) } + fetchedAlbums.append(album) } + // fetch all assets + self.fetchAlbumAsync(albums: albums) { [weak self] _ in + guard let `self` = self else { return } + // update sorted albums from albumMap + let fetched = Array(self.albumMap.values) + let albums = `self`.sortedAlbums(fromAlbums: fetched) + self.sortedAlbumsArray.removeAll() + self.sortedAlbumsArray.append(albums) + } + // get sorted albums let sortedAlbums = self.sortedAlbums(fromAlbums: fetchedAlbums) @@ -596,6 +612,27 @@ extension AssetsManager { return fetchResult } + func fetchAlbumAsync(albums: [PHAssetCollection], completion: @escaping (([PHFetchResult]) -> Void)) { + + self.albumLoadingQueue.async { + + var resuls: [PHFetchResult] = [] + + for album in albums { + let fetchResult = PHAsset.fetchAssets(in: album, options: self.pickerConfig.assetFetchOptions?[album.assetCollectionType]) + + // cache fetch result + self.fetchMap[album.localIdentifier] = fetchResult + + // cache album + self.albumMap[album.localIdentifier] = album + + resuls.append(fetchResult) + } + + completion(resuls) + } + } } // MARK: - IndexSet Utility From 63b77bbee81032b9d52d476b57ef8f4014e639a7 Mon Sep 17 00:00:00 2001 From: gezihuzi Date: Fri, 18 Sep 2020 16:05:50 +0800 Subject: [PATCH 09/20] Album asset asynchronous loading support --- .../AssetsAlbumViewController.swift | 19 ++++++++++++++++++- .../Classes/Assets/AssetsManager.swift | 16 ++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/AssetsPickerViewController/Classes/Album/Controller/AssetsAlbumViewController.swift b/AssetsPickerViewController/Classes/Album/Controller/AssetsAlbumViewController.swift index 4bfba35..8d87b16 100644 --- a/AssetsPickerViewController/Classes/Album/Controller/AssetsAlbumViewController.swift +++ b/AssetsPickerViewController/Classes/Album/Controller/AssetsAlbumViewController.swift @@ -111,12 +111,22 @@ open class AssetsAlbumViewController: UIViewController { if #available(iOS 11.0, *) { navigationController?.navigationBar.prefersLargeTitles = true } + + AssetsManager.shared.loadDelegate = self collectionView.snp.makeConstraints { (make) in make.edges.equalToSuperview() } - loadingPlaceholderView.isHidden = true + let isLoading = AssetsManager.shared.isLoading + if isLoading { + loadingActivityIndicatorView.startAnimating() + loadingPlaceholderView.isHidden = false + } else { + loadingActivityIndicatorView.stopAnimating() + loadingPlaceholderView.isHidden = true + } + if #available(iOS 13.0, *) { loadingPlaceholderView.backgroundColor = .systemBackground } else { @@ -367,3 +377,10 @@ extension AssetsAlbumViewController: AssetsManagerDelegate { public func assetsManager(manager: AssetsManager, updatedAssets assets: [PHAsset], at indexPaths: [IndexPath]) {} } +extension AssetsAlbumViewController: AssetsManagerLoadDelegate { + public func assetAsynchronousLoadingDidCompleted(_ manager: AssetsManager) { + collectionView.reloadData() + loadingActivityIndicatorView.stopAnimating() + loadingPlaceholderView.isHidden = true + } +} diff --git a/AssetsPickerViewController/Classes/Assets/AssetsManager.swift b/AssetsPickerViewController/Classes/Assets/AssetsManager.swift index b9d27f2..9237adb 100644 --- a/AssetsPickerViewController/Classes/Assets/AssetsManager.swift +++ b/AssetsPickerViewController/Classes/Assets/AssetsManager.swift @@ -26,6 +26,10 @@ public protocol AssetsManagerDelegate: class { func assetsManager(manager: AssetsManager, updatedAssets assets: [PHAsset], at indexPaths: [IndexPath]) } +public protocol AssetsManagerLoadDelegate: AnyObject { + func assetAsynchronousLoadingDidCompleted(_ manager: AssetsManager) +} + // MARK: - AssetsManager open class AssetsManager: NSObject { @@ -56,6 +60,9 @@ open class AssetsManager: NSObject { fileprivate(set) open var cameraRollAlbum: PHAssetCollection! fileprivate(set) open var selectedAlbum: PHAssetCollection? + fileprivate(set) open var isLoading: Bool = false + open weak var loadDelegate: AssetsManagerLoadDelegate? + fileprivate var isFetchedAlbums: Bool = false fileprivate var resourceLoadingQueue: DispatchQueue = DispatchQueue(label: "com.assetspicker.loader", qos: .userInitiated) fileprivate var albumLoadingQueue: DispatchQueue = DispatchQueue(label: "com.assetspicker.album.loader", qos: .default) @@ -548,6 +555,7 @@ extension AssetsManager { // fetch assets self.fetchAlbum(album: album) + fetchedAlbums.append(album) } // save alternative album if album.assetCollectionSubtype == .smartAlbumUserLibrary { @@ -555,11 +563,11 @@ extension AssetsManager { // fetch assets self.fetchAlbum(album: album) + fetchedAlbums.append(album) } - - fetchedAlbums.append(album) } + self.isLoading = true // fetch all assets self.fetchAlbumAsync(albums: albums) { [weak self] _ in guard let `self` = self else { return } @@ -568,6 +576,10 @@ extension AssetsManager { let albums = `self`.sortedAlbums(fromAlbums: fetched) self.sortedAlbumsArray.removeAll() self.sortedAlbumsArray.append(albums) + DispatchQueue.main.async { + self.isLoading = false + self.loadDelegate?.assetAsynchronousLoadingDidCompleted(self) + } } // get sorted albums From 48bde8a1375a3f5060b92c1991c713fbbe040321 Mon Sep 17 00:00:00 2001 From: gezihuzi Date: Fri, 18 Sep 2020 16:47:23 +0800 Subject: [PATCH 10/20] Improve album asset asynchronous loading support --- .../AssetsAlbumViewController.swift | 22 +-- .../Classes/Assets/AssetsManager.swift | 172 +++++++++++++++--- 2 files changed, 152 insertions(+), 42 deletions(-) diff --git a/AssetsPickerViewController/Classes/Album/Controller/AssetsAlbumViewController.swift b/AssetsPickerViewController/Classes/Album/Controller/AssetsAlbumViewController.swift index 8d87b16..6b64f52 100644 --- a/AssetsPickerViewController/Classes/Album/Controller/AssetsAlbumViewController.swift +++ b/AssetsPickerViewController/Classes/Album/Controller/AssetsAlbumViewController.swift @@ -111,20 +111,18 @@ open class AssetsAlbumViewController: UIViewController { if #available(iOS 11.0, *) { navigationController?.navigationBar.prefersLargeTitles = true } - - AssetsManager.shared.loadDelegate = self collectionView.snp.makeConstraints { (make) in make.edges.equalToSuperview() } - let isLoading = AssetsManager.shared.isLoading - if isLoading { - loadingActivityIndicatorView.startAnimating() - loadingPlaceholderView.isHidden = false - } else { + let isFetchedAlbums = AssetsManager.shared.isFetchedAlbums + if isFetchedAlbums { loadingActivityIndicatorView.stopAnimating() loadingPlaceholderView.isHidden = true + } else { + loadingActivityIndicatorView.startAnimating() + loadingPlaceholderView.isHidden = false } if #available(iOS 13.0, *) { @@ -343,6 +341,8 @@ extension AssetsAlbumViewController: AssetsManagerDelegate { public func assetsManagerFetched(manager: AssetsManager) { collectionView.reloadData() + loadingActivityIndicatorView.stopAnimating() + loadingPlaceholderView.isHidden = true } public func assetsManager(manager: AssetsManager, authorizationStatusChanged oldStatus: PHAuthorizationStatus, newStatus: PHAuthorizationStatus) {} @@ -376,11 +376,3 @@ extension AssetsAlbumViewController: AssetsManagerDelegate { public func assetsManager(manager: AssetsManager, removedAssets assets: [PHAsset], at indexPaths: [IndexPath]) {} public func assetsManager(manager: AssetsManager, updatedAssets assets: [PHAsset], at indexPaths: [IndexPath]) {} } - -extension AssetsAlbumViewController: AssetsManagerLoadDelegate { - public func assetAsynchronousLoadingDidCompleted(_ manager: AssetsManager) { - collectionView.reloadData() - loadingActivityIndicatorView.stopAnimating() - loadingPlaceholderView.isHidden = true - } -} diff --git a/AssetsPickerViewController/Classes/Assets/AssetsManager.swift b/AssetsPickerViewController/Classes/Assets/AssetsManager.swift index 9237adb..d9ce119 100644 --- a/AssetsPickerViewController/Classes/Assets/AssetsManager.swift +++ b/AssetsPickerViewController/Classes/Assets/AssetsManager.swift @@ -26,10 +26,6 @@ public protocol AssetsManagerDelegate: class { func assetsManager(manager: AssetsManager, updatedAssets assets: [PHAsset], at indexPaths: [IndexPath]) } -public protocol AssetsManagerLoadDelegate: AnyObject { - func assetAsynchronousLoadingDidCompleted(_ manager: AssetsManager) -} - // MARK: - AssetsManager open class AssetsManager: NSObject { @@ -60,10 +56,7 @@ open class AssetsManager: NSObject { fileprivate(set) open var cameraRollAlbum: PHAssetCollection! fileprivate(set) open var selectedAlbum: PHAssetCollection? - fileprivate(set) open var isLoading: Bool = false - open weak var loadDelegate: AssetsManagerLoadDelegate? - - fileprivate var isFetchedAlbums: Bool = false + fileprivate(set) var isFetchedAlbums: Bool = false fileprivate var resourceLoadingQueue: DispatchQueue = DispatchQueue(label: "com.assetspicker.loader", qos: .userInitiated) fileprivate var albumLoadingQueue: DispatchQueue = DispatchQueue(label: "com.assetspicker.album.loader", qos: .default) @@ -490,18 +483,22 @@ extension AssetsManager { guard let `self` = self else { return } if !self.isFetchedAlbums { - let smartAlbumEntry = self.fetchAlbums(forAlbumType: .smartAlbum) + var types: [PHAssetCollectionType] = [ .smartAlbum, .album ] + + let smartAlbumEntry = self.fetchDefaultAlbums(forAlbumType: .smartAlbum) self.fetchedAlbumsArray.append(smartAlbumEntry.fetchedAlbums) self.sortedAlbumsArray.append(smartAlbumEntry.sortedAlbums) self.albumsFetchArray.append(smartAlbumEntry.fetchResult) - let albumEntry = self.fetchAlbums(forAlbumType: .album) + let albumEntry = self.fetchDefaultAlbums(forAlbumType: .album) self.fetchedAlbumsArray.append(albumEntry.fetchedAlbums) self.sortedAlbumsArray.append(albumEntry.sortedAlbums) self.albumsFetchArray.append(albumEntry.fetchResult) if self.pickerConfig.albumIsShowMomentAlbums { - let momentEntry = self.fetchAlbums(forAlbumType: .moment) + types.append(.moment) + + let momentEntry = self.fetchDefaultAlbums(forAlbumType: .moment) self.fetchedAlbumsArray.append(momentEntry.fetchedAlbums) self.sortedAlbumsArray.append(momentEntry.sortedAlbums) self.albumsFetchArray.append(momentEntry.fetchResult) @@ -512,7 +509,35 @@ extension AssetsManager { delegate.assetsManagerFetched(manager: self) } } - self.isFetchedAlbums = true + + let group = DispatchGroup() + + self.albumLoadingQueue.async { + var fetchedAlbumsArray: [[PHAssetCollection]] = [] + var sortedAlbumsArray: [[PHAssetCollection]] = [] + var albumsFetchArray: [PHFetchResult] = [] + + for type in types { + group.enter() + self.fetchAlbumsAsync(forAlbumType: type) { (entry) in + fetchedAlbumsArray.append(entry.fetchedAlbums) + sortedAlbumsArray.append(entry.sortedAlbums) + albumsFetchArray.append(entry.fetchResult) + group.leave() + } + } + self.fetchedAlbumsArray = fetchedAlbumsArray + self.sortedAlbumsArray = sortedAlbumsArray + self.albumsFetchArray = albumsFetchArray + + self.subscribers.forEach { [weak self] (delegate) in + guard let `self` = self else { return } + DispatchQueue.main.async { + delegate.assetsManagerFetched(manager: self) + } + } + self.isFetchedAlbums = true + } } // notify DispatchQueue.main.async { @@ -538,7 +563,7 @@ extension AssetsManager { } - func fetchAlbums(forAlbumType type: PHAssetCollectionType) -> (fetchedAlbums: [PHAssetCollection], sortedAlbums: [PHAssetCollection], fetchResult: PHFetchResult) { + func fetchDefaultAlbums(forAlbumType type: PHAssetCollectionType) -> (fetchedAlbums: [PHAssetCollection], sortedAlbums: [PHAssetCollection], fetchResult: PHFetchResult) { let fetchOption = pickerConfig.albumFetchOptions?[type] let albumFetchResult = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: fetchOption) @@ -555,7 +580,6 @@ extension AssetsManager { // fetch assets self.fetchAlbum(album: album) - fetchedAlbums.append(album) } // save alternative album if album.assetCollectionSubtype == .smartAlbumUserLibrary { @@ -563,23 +587,62 @@ extension AssetsManager { // fetch assets self.fetchAlbum(album: album) - fetchedAlbums.append(album) } + + fetchedAlbums.append(album) } - self.isLoading = true - // fetch all assets - self.fetchAlbumAsync(albums: albums) { [weak self] _ in - guard let `self` = self else { return } - // update sorted albums from albumMap - let fetched = Array(self.albumMap.values) - let albums = `self`.sortedAlbums(fromAlbums: fetched) - self.sortedAlbumsArray.removeAll() - self.sortedAlbumsArray.append(albums) - DispatchQueue.main.async { - self.isLoading = false - self.loadDelegate?.assetAsynchronousLoadingDidCompleted(self) + // get sorted albums + let sortedAlbums = self.sortedAlbums(fromAlbums: fetchedAlbums) + + // set default album + if let defaultAlbum = self.defaultAlbum { + logi("Default album is \"\(defaultAlbum.localizedTitle ?? "")\"") + } else { + if let defaultAlbum = self.defaultAlbum { + logi("Set default album \"\(defaultAlbum.localizedTitle ?? "")\"") + } else { + if let cameraRollAlbum = self.cameraRollAlbum { + self.defaultAlbum = cameraRollAlbum + logw("Set default album with fallback default album \"\(cameraRollAlbum.localizedTitle ?? "")\"") + } else { + if let firstAlbum = sortedAlbums.first, type == .smartAlbum { + self.defaultAlbum = firstAlbum + loge("Set default album with first item \"\(firstAlbum.localizedTitle ?? "")\"") + } else { + logc("Is this case could happen? Please raise an issue if you've met this message.") + } + } + } + } + + // append album fetch result + return (fetchedAlbums, sortedAlbums, albumFetchResult) + } + + func fetchAlbums(forAlbumType type: PHAssetCollectionType) -> (fetchedAlbums: [PHAssetCollection], sortedAlbums: [PHAssetCollection], fetchResult: PHFetchResult) { + + let fetchOption = pickerConfig.albumFetchOptions?[type] + let albumFetchResult = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: fetchOption) + var fetchedAlbums = [PHAssetCollection]() + + let indexSet = IndexSet(integersIn: 0..)) -> Void) { + + let fetchOption = pickerConfig.albumFetchOptions?[type] + let albumFetchResult = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: fetchOption) + var fetchedAlbums = [PHAssetCollection]() + + let indexSet = IndexSet(integersIn: 0.. PHFetchResult { From 88ea8e326d0fd376cab8e483849f739541fac3de Mon Sep 17 00:00:00 2001 From: gezihuzi Date: Fri, 18 Sep 2020 22:51:45 +0800 Subject: [PATCH 11/20] Improve album asset asynchronous loading support --- .../Classes/Assets/AssetsManager.swift | 167 +++++++++++++----- 1 file changed, 118 insertions(+), 49 deletions(-) diff --git a/AssetsPickerViewController/Classes/Assets/AssetsManager.swift b/AssetsPickerViewController/Classes/Assets/AssetsManager.swift index d9ce119..bdf3fe8 100644 --- a/AssetsPickerViewController/Classes/Assets/AssetsManager.swift +++ b/AssetsPickerViewController/Classes/Assets/AssetsManager.swift @@ -26,6 +26,10 @@ public protocol AssetsManagerDelegate: class { func assetsManager(manager: AssetsManager, updatedAssets assets: [PHAsset], at indexPaths: [IndexPath]) } +typealias AssetsFetchEntry = (albums: [PHFetchResult], fetchMap: [String: PHFetchResult], albumMap: [String: PHAssetCollection]) +typealias AssetsAlbumEntry = (fetchedAlbums: [PHAssetCollection], sortedAlbums: [PHAssetCollection], fetchResult: PHFetchResult) +typealias AssetsAlbumArrayEntry = (fetchedAlbumsArray: [[PHAssetCollection]], sortedAlbumsArray: [[PHAssetCollection]], albumsFetchArray: [PHFetchResult]) + // MARK: - AssetsManager open class AssetsManager: NSObject { @@ -377,6 +381,19 @@ extension AssetsManager { return true } + func isQualified(album: PHAssetCollection, filterBy fetchMap: [String: PHFetchResult]) -> Bool { + if let albumFilter = pickerConfig.albumFilter?[album.assetCollectionType], let fetchResult = fetchMap[album.localIdentifier] { + return albumFilter(album, fetchResult) + } + guard self.pickerConfig.albumIsShowHiddenAlbum || album.assetCollectionSubtype != .smartAlbumAllHidden else { + return false + } + guard let fetchResult = fetchMap[album.localIdentifier], self.pickerConfig.albumIsShowEmptyAlbum || fetchResult.count > 0 else { + return false + } + return true + } + func remove(album: PHAssetCollection? = nil, indexPath: IndexPath? = nil) { if let indexPath = indexPath { fetchedAlbumsArray[indexPath.section].remove(at: indexPath.row) @@ -415,6 +432,31 @@ extension AssetsManager { } } } + + func sortedAlbums(fromAlbums albums: [PHAssetCollection], filterBy fetchMap: [String: PHFetchResult]) -> [PHAssetCollection] { + guard let albumType = albums.first?.assetCollectionType else { + return albums + } + let filtered = albums.filter { self.isQualified(album: $0, filterBy: fetchMap) } + if let comparator = pickerConfig.albumComparator { + return filtered.sorted(by: { (leftAlbum, rightAlbum) -> Bool in + if let leftResult = fetchMap[leftAlbum.localIdentifier], let rightResult = fetchMap[rightAlbum.localIdentifier] { + return comparator(leftAlbum.assetCollectionType, (leftAlbum, leftResult), (rightAlbum, rightResult)) + } else { + logw("Failed to get fetch result from fetchMap. Please raise an issue if you've met this message.") + return true + } + }) + } else { + if let _ = pickerConfig.albumFetchOptions?[albumType] { + // return fetched album as it is + return filtered + } else { + // default: by count + return filtered.sorted(by: { Int((fetchMap[$0.localIdentifier]?.count) ?? 0) > Int((fetchMap[$1.localIdentifier]?.count) ?? 0) }) + } + } + } } // MARK: - Check @@ -510,26 +552,11 @@ extension AssetsManager { } } - let group = DispatchGroup() - - self.albumLoadingQueue.async { - var fetchedAlbumsArray: [[PHAssetCollection]] = [] - var sortedAlbumsArray: [[PHAssetCollection]] = [] - var albumsFetchArray: [PHFetchResult] = [] - - for type in types { - group.enter() - self.fetchAlbumsAsync(forAlbumType: type) { (entry) in - fetchedAlbumsArray.append(entry.fetchedAlbums) - sortedAlbumsArray.append(entry.sortedAlbums) - albumsFetchArray.append(entry.fetchResult) - group.leave() - } - } - self.fetchedAlbumsArray = fetchedAlbumsArray - self.sortedAlbumsArray = sortedAlbumsArray - self.albumsFetchArray = albumsFetchArray - + self.fetchAllAlbums(types: types) { (result) in + self.fetchedAlbumsArray = result.fetchedAlbumsArray + self.sortedAlbumsArray = result.sortedAlbumsArray + self.albumsFetchArray = result.albumsFetchArray + self.subscribers.forEach { [weak self] (delegate) in guard let `self` = self else { return } DispatchQueue.main.async { @@ -546,6 +573,43 @@ extension AssetsManager { } } + func fetchAllAlbums(types: [PHAssetCollectionType], complection: @escaping (AssetsAlbumArrayEntry) -> Void ) { + + let queue = DispatchQueue.global(qos: .userInitiated) + + var fetchedAlbumsArray: [[PHAssetCollection]] = [] + var sortedAlbumsArray: [[PHAssetCollection]] = [] + var albumsFetchArray: [PHFetchResult] = [] + + let group = DispatchGroup() + + var fetchMap = [String: PHFetchResult]() + var albumMap = [String: PHAssetCollection]() + + for type in types { + queue.async(group: group) { + group.enter() + self.fetchAlbumsAsync(forAlbumType: type) { (albumsArrayEntry, fetchedEntry) in + fetchedAlbumsArray.append(albumsArrayEntry.fetchedAlbums) + sortedAlbumsArray.append(albumsArrayEntry.sortedAlbums) + albumsFetchArray.append(albumsArrayEntry.fetchResult) + + fetchMap = fetchMap.merging(fetchedEntry.fetchMap) { (first, second) -> PHFetchResult in return first } + albumMap = albumMap.merging(fetchedEntry.albumMap) { (first, second) -> PHAssetCollection in return first } + + group.leave() + } + } + } + + group.notify(queue: .main) { + self.fetchMap = fetchMap + self.albumMap = albumMap + let result = (fetchedAlbumsArray, sortedAlbumsArray, albumsFetchArray) + complection(result) + } + } + open func fetchAssets(isRefetch: Bool = false, completion: ((PHFetchResult?) -> Void)? = nil) { fetchAlbums(isRefetch: isRefetch, completion: { [weak self] _ in @@ -580,6 +644,8 @@ extension AssetsManager { // fetch assets self.fetchAlbum(album: album) + + fetchedAlbums.append(album) } // save alternative album if album.assetCollectionSubtype == .smartAlbumUserLibrary { @@ -587,9 +653,9 @@ extension AssetsManager { // fetch assets self.fetchAlbum(album: album) + + fetchedAlbums.append(album) } - - fetchedAlbums.append(album) } // get sorted albums @@ -673,7 +739,7 @@ extension AssetsManager { return (fetchedAlbums, sortedAlbums, albumFetchResult) } - func fetchAlbumsAsync(forAlbumType type: PHAssetCollectionType, complection: ((fetchedAlbums: [PHAssetCollection], sortedAlbums: [PHAssetCollection], fetchResult: PHFetchResult)) -> Void) { + func fetchAlbumsAsync(forAlbumType type: PHAssetCollectionType, complection: @escaping (AssetsAlbumEntry, AssetsFetchEntry) -> Void) { let fetchOption = pickerConfig.albumFetchOptions?[type] let albumFetchResult = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: fetchOption) @@ -683,8 +749,6 @@ extension AssetsManager { let albums = albumFetchResult.objects(at: indexSet) for album in albums { - // fetch assets - self.fetchAlbum(album: album) // set default album if album.assetCollectionSubtype == self.pickerConfig.albumDefaultType { @@ -698,34 +762,37 @@ extension AssetsManager { fetchedAlbums.append(album) } - // get sorted albums - let sortedAlbums = self.sortedAlbums(fromAlbums: fetchedAlbums) - - // set default album - if let defaultAlbum = self.defaultAlbum { - logi("Default album is \"\(defaultAlbum.localizedTitle ?? "")\"") - } else { + self.fetchAlbumAsync(albums: fetchedAlbums) { entry in + + // get sorted albums + let sortedAlbums = self.sortedAlbums(fromAlbums: fetchedAlbums, filterBy: entry.fetchMap) + + // set default album if let defaultAlbum = self.defaultAlbum { - logi("Set default album \"\(defaultAlbum.localizedTitle ?? "")\"") + logi("Default album is \"\(defaultAlbum.localizedTitle ?? "")\"") } else { - if let cameraRollAlbum = self.cameraRollAlbum { - self.defaultAlbum = cameraRollAlbum - logw("Set default album with fallback default album \"\(cameraRollAlbum.localizedTitle ?? "")\"") + if let defaultAlbum = self.defaultAlbum { + logi("Set default album \"\(defaultAlbum.localizedTitle ?? "")\"") } else { - if let firstAlbum = sortedAlbums.first, type == .smartAlbum { - self.defaultAlbum = firstAlbum - loge("Set default album with first item \"\(firstAlbum.localizedTitle ?? "")\"") + if let cameraRollAlbum = self.cameraRollAlbum { + self.defaultAlbum = cameraRollAlbum + logw("Set default album with fallback default album \"\(cameraRollAlbum.localizedTitle ?? "")\"") } else { - logc("Is this case could happen? Please raise an issue if you've met this message.") + if let firstAlbum = sortedAlbums.first, type == .smartAlbum { + self.defaultAlbum = firstAlbum + loge("Set default album with first item \"\(firstAlbum.localizedTitle ?? "")\"") + } else { + logc("Is this case could happen? Please raise an issue if you've met this message.") + } } } } + + let result = (fetchedAlbums, sortedAlbums, albumFetchResult) + + // append album fetch result + complection(result, entry) } - - let result = (fetchedAlbums, sortedAlbums, albumFetchResult) - - // append album fetch result - complection(result) } @discardableResult @@ -742,25 +809,27 @@ extension AssetsManager { return fetchResult } - func fetchAlbumAsync(albums: [PHAssetCollection], completion: @escaping (([PHFetchResult]) -> Void)) { + func fetchAlbumAsync(albums: [PHAssetCollection], completion: @escaping (AssetsFetchEntry) -> Void) { self.albumLoadingQueue.async { var resuls: [PHFetchResult] = [] + var fetchMap = [String: PHFetchResult]() + var albumMap = [String: PHAssetCollection]() for album in albums { let fetchResult = PHAsset.fetchAssets(in: album, options: self.pickerConfig.assetFetchOptions?[album.assetCollectionType]) // cache fetch result - self.fetchMap[album.localIdentifier] = fetchResult + fetchMap[album.localIdentifier] = fetchResult // cache album - self.albumMap[album.localIdentifier] = album + albumMap[album.localIdentifier] = album resuls.append(fetchResult) } - completion(resuls) + completion((resuls, fetchMap, albumMap)) } } } From 7e7cf0b344e7a2f4c6d1d54ad890dfe55f4d7e1c Mon Sep 17 00:00:00 2001 From: gezihuzi Date: Fri, 18 Sep 2020 22:54:34 +0800 Subject: [PATCH 12/20] Refine the display of the loading progress indicator --- .../Album/Controller/AssetsAlbumViewController.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/AssetsPickerViewController/Classes/Album/Controller/AssetsAlbumViewController.swift b/AssetsPickerViewController/Classes/Album/Controller/AssetsAlbumViewController.swift index 6b64f52..adc9396 100644 --- a/AssetsPickerViewController/Classes/Album/Controller/AssetsAlbumViewController.swift +++ b/AssetsPickerViewController/Classes/Album/Controller/AssetsAlbumViewController.swift @@ -341,8 +341,14 @@ extension AssetsAlbumViewController: AssetsManagerDelegate { public func assetsManagerFetched(manager: AssetsManager) { collectionView.reloadData() - loadingActivityIndicatorView.stopAnimating() - loadingPlaceholderView.isHidden = true + let isFetchedAlbums = AssetsManager.shared.isFetchedAlbums + if isFetchedAlbums { + loadingActivityIndicatorView.stopAnimating() + loadingPlaceholderView.isHidden = true + } else { + loadingActivityIndicatorView.startAnimating() + loadingPlaceholderView.isHidden = false + } } public func assetsManager(manager: AssetsManager, authorizationStatusChanged oldStatus: PHAuthorizationStatus, newStatus: PHAuthorizationStatus) {} From af345154295f584d6fff07e9d3cddc5e14391c42 Mon Sep 17 00:00:00 2001 From: Esteban Brenes Date: Fri, 25 Sep 2020 15:26:47 -0600 Subject: [PATCH 13/20] fix issue with done button and deselection --- .../Photo/Controller/AssetsPhotoViewController+UI.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+UI.swift b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+UI.swift index 686ab32..443e07d 100644 --- a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+UI.swift +++ b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController+UI.swift @@ -134,13 +134,11 @@ extension AssetsPhotoViewController { } func updateNavigationStatus() { + doneButtonItem.isEnabled = selectedArray.count >= (pickerConfig.assetsMinimumSelectionCount >= 0 ? pickerConfig.assetsMinimumSelectionCount : 1) if let album = AssetsManager.shared.selectedAlbum, selectedArray.isEmpty { title = self.title(forAlbum: album) } else { - - doneButtonItem.isEnabled = selectedArray.count >= (pickerConfig.assetsMinimumSelectionCount >= 0 ? pickerConfig.assetsMinimumSelectionCount : 1) - let counts: (imageCount: Int, videoCount: Int) = selectedArray.reduce((0, 0)) { (result, asset) -> (Int, Int) in let imageCount = asset.mediaType == .image ? 1 : 0 let videoCount = asset.mediaType == .video ? 1 : 0 From 80532057c3d0d6c07de94e64cb3a359b69e9b463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Bu=CC=88nemann?= Date: Sat, 26 Sep 2020 02:04:45 +0200 Subject: [PATCH 14/20] Add missing UIKit imports --- .../Classes/Library/Extensions/UIView+Dimmer.swift | 1 + .../Classes/Photo/View/PanoramaIconView.swift | 1 + .../Classes/Picker/AssetsCameraManager.swift | 1 + .../Classes/Utility/Device+AssetsPickerViewController.swift | 1 + .../Classes/Utility/UIColor+AssetsPickerViewController.swift | 1 + 5 files changed, 5 insertions(+) diff --git a/AssetsPickerViewController/Classes/Library/Extensions/UIView+Dimmer.swift b/AssetsPickerViewController/Classes/Library/Extensions/UIView+Dimmer.swift index 43eef5f..8dbd70e 100644 --- a/AssetsPickerViewController/Classes/Library/Extensions/UIView+Dimmer.swift +++ b/AssetsPickerViewController/Classes/Library/Extensions/UIView+Dimmer.swift @@ -6,6 +6,7 @@ // // +import UIKit import SnapKit fileprivate let kDimmerViewKey = "kDimmerViewKey" diff --git a/AssetsPickerViewController/Classes/Photo/View/PanoramaIconView.swift b/AssetsPickerViewController/Classes/Photo/View/PanoramaIconView.swift index 9182127..5ad7a4d 100644 --- a/AssetsPickerViewController/Classes/Photo/View/PanoramaIconView.swift +++ b/AssetsPickerViewController/Classes/Photo/View/PanoramaIconView.swift @@ -6,6 +6,7 @@ // import Foundation +import UIKit open class PanoramaIconView: UIView { diff --git a/AssetsPickerViewController/Classes/Picker/AssetsCameraManager.swift b/AssetsPickerViewController/Classes/Picker/AssetsCameraManager.swift index 3d027ff..2a63ed1 100644 --- a/AssetsPickerViewController/Classes/Picker/AssetsCameraManager.swift +++ b/AssetsPickerViewController/Classes/Picker/AssetsCameraManager.swift @@ -8,6 +8,7 @@ import Foundation import AVFoundation import Photos +import UIKit protocol AssetsPickerManagerDelegate: NSObject { func assetsPickerManagerSavedAsset(identifier: String) diff --git a/AssetsPickerViewController/Classes/Utility/Device+AssetsPickerViewController.swift b/AssetsPickerViewController/Classes/Utility/Device+AssetsPickerViewController.swift index ea5cbe0..9aaa78d 100644 --- a/AssetsPickerViewController/Classes/Utility/Device+AssetsPickerViewController.swift +++ b/AssetsPickerViewController/Classes/Utility/Device+AssetsPickerViewController.swift @@ -5,6 +5,7 @@ // Created by DragonCherry on 19/12/2017. // +import UIKit import Device extension Device { diff --git a/AssetsPickerViewController/Classes/Utility/UIColor+AssetsPickerViewController.swift b/AssetsPickerViewController/Classes/Utility/UIColor+AssetsPickerViewController.swift index a18fcfb..d0436d4 100644 --- a/AssetsPickerViewController/Classes/Utility/UIColor+AssetsPickerViewController.swift +++ b/AssetsPickerViewController/Classes/Utility/UIColor+AssetsPickerViewController.swift @@ -6,6 +6,7 @@ // import Foundation +import UIKit extension UIColor { From f34bf7bdf6fa91fd3a749b8e3b04a6665cc4fe61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Bu=CC=88nemann?= Date: Sat, 26 Sep 2020 09:07:22 +0200 Subject: [PATCH 15/20] Remove dependency on Device library --- AssetsPickerViewController.podspec | 1 - .../AssetsPhotoViewController.swift | 1 - .../Photo/View/AssetsPhotoLayout.swift | 9 ++++---- .../Device+AssetsPickerViewController.swift | 21 ------------------- .../UIScreen+AssetsPickerViewController.swift | 21 ++++++++++++++----- .../project.pbxproj | 2 -- Example/Podfile.lock | 14 +++++-------- 7 files changed, 25 insertions(+), 44 deletions(-) delete mode 100644 AssetsPickerViewController/Classes/Utility/Device+AssetsPickerViewController.swift diff --git a/AssetsPickerViewController.podspec b/AssetsPickerViewController.podspec index f1cfe94..7d9f444 100644 --- a/AssetsPickerViewController.podspec +++ b/AssetsPickerViewController.podspec @@ -41,5 +41,4 @@ Fully customizable UI. # s.public_header_files = 'Pod/Classes/**/*.h' # s.frameworks = 'UIKit', 'MapKit' s.dependency 'SnapKit' - s.dependency 'Device' end diff --git a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController.swift b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController.swift index b2848e4..48a7ea8 100644 --- a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController.swift +++ b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController.swift @@ -9,7 +9,6 @@ import UIKit import Photos import PhotosUI -import Device import SnapKit // MARK: - AssetsPhotoViewController diff --git a/AssetsPickerViewController/Classes/Photo/View/AssetsPhotoLayout.swift b/AssetsPickerViewController/Classes/Photo/View/AssetsPhotoLayout.swift index f6f63f8..3ec344a 100644 --- a/AssetsPickerViewController/Classes/Photo/View/AssetsPhotoLayout.swift +++ b/AssetsPickerViewController/Classes/Photo/View/AssetsPhotoLayout.swift @@ -7,7 +7,6 @@ // import UIKit -import Device open class AssetsPhotoLayout: UICollectionViewFlowLayout { @@ -46,13 +45,13 @@ extension AssetsPhotoLayout { let cellSize = isPortrait ? pickerConfig.assetPortraitCellSize(forViewSize: UIScreen.main.portraitContentSize) : pickerConfig.assetLandscapeCellSize(forViewSize: UIScreen.main.landscapeContentSize) let lineSpace = isPortrait ? pickerConfig.assetPortraitLineSpace : pickerConfig.assetLandscapeLineSpace let contentHeight = CGFloat(rows) * cellSize.height + (CGFloat(max(rows - 1, 0)) * lineSpace) - let bottomHeight = cellSize.height * 2/3 + Device.safeAreaInsets(isPortrait: isPortrait).bottom + let bottomHeight = cellSize.height * 2/3 + UIScreen.safeAreaInsets(isPortrait: isPortrait).bottom return contentHeight + bottomHeight } private func offsetRatio(collectionView: UICollectionView, offset: CGPoint, contentSize: CGSize, isPortrait: Bool) -> CGFloat { - return (offset.y > 0 ? offset.y : 0) / ((contentSize.height + Device.safeAreaInsets(isPortrait: isPortrait).bottom) - collectionView.bounds.height) + return (offset.y > 0 ? offset.y : 0) / ((contentSize.height + UIScreen.safeAreaInsets(isPortrait: isPortrait).bottom) - collectionView.bounds.height) } open func translateOffset(forChangingSize size: CGSize, currentOffset: CGPoint) -> CGPoint? { @@ -67,8 +66,8 @@ extension AssetsPhotoLayout { var futureOffsetY = (contentHeight - size.height) * currentRatio if currentOffset.y < 0 { - let insetRatio = (-currentOffset.y) / Device.safeAreaInsets(isPortrait: isPortraitCurrent).top - let insetDiff = Device.safeAreaInsets(isPortrait: isPortraitFuture).top * insetRatio + let insetRatio = (-currentOffset.y) / UIScreen.safeAreaInsets(isPortrait: isPortraitCurrent).top + let insetDiff = UIScreen.safeAreaInsets(isPortrait: isPortraitFuture).top * insetRatio futureOffsetY -= insetDiff } diff --git a/AssetsPickerViewController/Classes/Utility/Device+AssetsPickerViewController.swift b/AssetsPickerViewController/Classes/Utility/Device+AssetsPickerViewController.swift deleted file mode 100644 index 9aaa78d..0000000 --- a/AssetsPickerViewController/Classes/Utility/Device+AssetsPickerViewController.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Device+AssetsPickerViewController.swift -// AssetsPickerViewController -// -// Created by DragonCherry on 19/12/2017. -// - -import UIKit -import Device - -extension Device { - static func safeAreaInsets(isPortrait: Bool) -> UIEdgeInsets { - let size = Device.size() - switch size { - case .screen5_8Inch: - return isPortrait ? UIEdgeInsets(top: 88, left: 0, bottom: 34, right: 0) : UIEdgeInsets(top: 32, left: 44, bottom: 21, right: 44) - default: - return isPortrait ? UIEdgeInsets(top: 64, left: 0, bottom: 0, right: 0) : UIEdgeInsets(top: 30, left: 0, bottom: 0, right: 0) - } - } -} diff --git a/AssetsPickerViewController/Classes/Utility/UIScreen+AssetsPickerViewController.swift b/AssetsPickerViewController/Classes/Utility/UIScreen+AssetsPickerViewController.swift index e1dcd3b..0c4a9ec 100644 --- a/AssetsPickerViewController/Classes/Utility/UIScreen+AssetsPickerViewController.swift +++ b/AssetsPickerViewController/Classes/Utility/UIScreen+AssetsPickerViewController.swift @@ -7,9 +7,20 @@ // import UIKit -import Device extension UIScreen { + static func safeAreaInsets(isPortrait: Bool) -> UIEdgeInsets { + let w: Double = Double(UIScreen.main.bounds.width) + let h: Double = Double(UIScreen.main.bounds.height) + let screenHeight: Double = max(w, h) + + switch screenHeight { + case 812: // 5.8" (iPhone X/XS/XR/11) + return isPortrait ? UIEdgeInsets(top: 88, left: 0, bottom: 34, right: 0) : UIEdgeInsets(top: 32, left: 44, bottom: 21, right: 44) + default: + return isPortrait ? UIEdgeInsets(top: 64, left: 0, bottom: 0, right: 0) : UIEdgeInsets(top: 30, left: 0, bottom: 0, right: 0) + } + } var portraitSize: CGSize { let size = UIScreen.main.bounds.size @@ -24,8 +35,8 @@ extension UIScreen { var portraitContentSize: CGSize { var size = UIScreen.main.portraitSize if #available(iOS 11.0, *) { - size.width -= Device.safeAreaInsets(isPortrait: true).left + Device.safeAreaInsets(isPortrait: true).right - size.height -= Device.safeAreaInsets(isPortrait: true).top + Device.safeAreaInsets(isPortrait: true).bottom + size.width -= UIScreen.safeAreaInsets(isPortrait: true).left + UIScreen.safeAreaInsets(isPortrait: true).right + size.height -= UIScreen.safeAreaInsets(isPortrait: true).top + UIScreen.safeAreaInsets(isPortrait: true).bottom } return size } @@ -33,8 +44,8 @@ extension UIScreen { var landscapeContentSize: CGSize { var size = UIScreen.main.landscapeSize if #available(iOS 11.0, *) { - size.width -= Device.safeAreaInsets(isPortrait: false).left + Device.safeAreaInsets(isPortrait: false).right - size.height -= Device.safeAreaInsets(isPortrait: false).top + Device.safeAreaInsets(isPortrait: false).bottom + size.width -= UIScreen.safeAreaInsets(isPortrait: false).left + UIScreen.safeAreaInsets(isPortrait: false).right + size.height -= UIScreen.safeAreaInsets(isPortrait: false).top + UIScreen.safeAreaInsets(isPortrait: false).bottom } return size } diff --git a/Example/AssetsPickerViewController.xcodeproj/project.pbxproj b/Example/AssetsPickerViewController.xcodeproj/project.pbxproj index d060ebb..d976757 100644 --- a/Example/AssetsPickerViewController.xcodeproj/project.pbxproj +++ b/Example/AssetsPickerViewController.xcodeproj/project.pbxproj @@ -344,13 +344,11 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-AssetsPickerViewController_Example/Pods-AssetsPickerViewController_Example-frameworks.sh", "${BUILT_PRODUCTS_DIR}/AssetsPickerViewController/AssetsPickerViewController.framework", - "${BUILT_PRODUCTS_DIR}/Device/Device.framework", "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AssetsPickerViewController.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Device.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 2efd7c2..16aca5c 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,16 +1,13 @@ PODS: - - AssetsPickerViewController (2.9.2): - - Device + - AssetsPickerViewController (2.9.6): - SnapKit - - Device (3.1.2) - - SnapKit (5.0.0) + - SnapKit (5.0.1) DEPENDENCIES: - AssetsPickerViewController (from `../`) SPEC REPOS: - https://github.com/CocoaPods/Specs.git: - - Device + trunk: - SnapKit EXTERNAL SOURCES: @@ -18,9 +15,8 @@ EXTERNAL SOURCES: :path: "../" SPEC CHECKSUMS: - AssetsPickerViewController: 6626c9640d75ee4b60eea9b0fddf95cac0f41124 - Device: 62242076214c30fb5760174b3601cefafa70a481 - SnapKit: fd22d10eb9aff484d79a8724eab922c1ddf89bcf + AssetsPickerViewController: afad53707ec66ddd15182fa62630448736ed3976 + SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb PODFILE CHECKSUM: ed535a80c7e9986ea173935163bbd69a1c717311 From c23fd76727669d12bff798c678171816b8c2e338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Bu=CC=88nemann?= Date: Sat, 26 Sep 2020 14:12:09 +0200 Subject: [PATCH 16/20] Add SwiftPM manifest and localized string lookup --- .gitignore | 2 ++ .../String+AssetsPickerViewController.swift | 4 +++ Package.swift | 27 +++++++++++++++++++ README.md | 4 +-- 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 Package.swift diff --git a/.gitignore b/.gitignore index d534044..1bf95b1 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +37,9 @@ playground.xcworkspace # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ # Package.pins +Package.resolved .build/ +.swiftpm/ # CocoaPods # diff --git a/AssetsPickerViewController/Classes/Utility/String+AssetsPickerViewController.swift b/AssetsPickerViewController/Classes/Utility/String+AssetsPickerViewController.swift index 53fe3b7..71e21db 100644 --- a/AssetsPickerViewController/Classes/Utility/String+AssetsPickerViewController.swift +++ b/AssetsPickerViewController/Classes/Utility/String+AssetsPickerViewController.swift @@ -15,7 +15,11 @@ extension String { let customConfig = AssetsPickerConfig.customStringConfig, let localizedKey = AssetsPickerLocalizedStringKey(rawValue: key), let string = customConfig[localizedKey] else { +#if SWIFT_PACKAGE + self = Bundle.module.localizedString(forKey: key, value: key, table: "AssetsPickerViewController") +#else self = Bundle.assetsPickerBundle.localizedString(forKey: key, value: key, table: "AssetsPickerViewController") +#endif return } self = string diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..469d170 --- /dev/null +++ b/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version:5.3 +import PackageDescription + +let package = Package( + name: "AssetsPickerViewController", + defaultLocalization: "en", + platforms: [.iOS(.v10)], + products: [ + .library( + name: "AssetsPickerViewController", + targets: ["AssetsPickerViewController"] + ) + ], + dependencies: [ + .package(url: "https://github.com/SnapKit/SnapKit", from: "5.0.1") + ], + targets: [ + .target( + name: "AssetsPickerViewController", + dependencies: ["SnapKit"], + path: "AssetsPickerViewController", + sources: ["Classes"], + resources: [.process("Assets")] + ) + ], + swiftLanguageVersions: [.v4_2, .v5] +) diff --git a/README.md b/README.md index 35455fe..4e64d15 100644 --- a/README.md +++ b/README.md @@ -101,10 +101,10 @@ Customizable Album & Asset Layout - multiple selection by dragging cells (from iOS 13) +- SPM(Swift Package Manager) support -## Features To-do -- SPM(Swift Package Manager) support +## Features To-do - Cropping image before select From 38e26e34e8ca943d22595231ba9c180d6fb9bfd5 Mon Sep 17 00:00:00 2001 From: DragonCherry Date: Tue, 29 Sep 2020 15:51:49 +0900 Subject: [PATCH 17/20] version up --- AssetsPickerViewController.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AssetsPickerViewController.podspec b/AssetsPickerViewController.podspec index 7d9f444..92f0124 100644 --- a/AssetsPickerViewController.podspec +++ b/AssetsPickerViewController.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'AssetsPickerViewController' - s.version = '2.9.6' + s.version = '2.9.7' s.summary = 'Picker controller that supports multiple photos and videos written in Swift.' # This description is used to generate tags and improve search results. From 6adb12506cb27875599a1145a94fd506e53839e8 Mon Sep 17 00:00:00 2001 From: DragonCherry Date: Tue, 29 Sep 2020 15:57:05 +0900 Subject: [PATCH 18/20] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4e64d15..633a856 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# Now iOS 14 supports multiple asset picker by default. I recommend use PHPickerViewController instead of this picker for many reasons. +# Now iOS 14 supports multiple asset picker by default. I recommend use PHPickerViewController from iOS 14 instead of this, it's a much better solution. ## AssetsPickerViewController From 2d0819b450fc13e42ce80cdf69daf594504ae1be Mon Sep 17 00:00:00 2001 From: Ravi Agrawal Date: Thu, 26 Nov 2020 13:02:34 +0530 Subject: [PATCH 19/20] resolves objective-c issues while calling methods from objective-c classes --- .../Classes/Picker/AssetsPickerConfig.swift | 12 +++++++++--- .../Controller/AssetsPickerViewController.swift | 1 + 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/AssetsPickerViewController/Classes/Picker/AssetsPickerConfig.swift b/AssetsPickerViewController/Classes/Picker/AssetsPickerConfig.swift index 5519cda..68f3e61 100644 --- a/AssetsPickerViewController/Classes/Picker/AssetsPickerConfig.swift +++ b/AssetsPickerViewController/Classes/Picker/AssetsPickerConfig.swift @@ -9,7 +9,8 @@ import UIKit import Photos -open class AssetsPickerConfig { +@objcMembers +open class AssetsPickerConfig : NSObject { // MARK: - Localized Strings Config @@ -93,6 +94,7 @@ open class AssetsPickerConfig { open var assetIsForcedSelectAssetFromCamera: Bool = true // MARK: Fetch + open var isVideoAllowed: Bool = false open var assetFetchOptions: [PHAssetCollectionType: PHFetchOptions]? // MARK: Custom Layout @@ -126,7 +128,7 @@ open class AssetsPickerConfig { return CGSize(width: edge, height: edge) } - public init() {} + public override init() {} @discardableResult open func prepare() -> Self { @@ -180,7 +182,11 @@ open class AssetsPickerConfig { NSSortDescriptor(key: "creationDate", ascending: true), NSSortDescriptor(key: "modificationDate", ascending: true) ] - options.predicate = NSPredicate(format: "mediaType = %d OR mediaType = %d", PHAssetMediaType.image.rawValue, PHAssetMediaType.video.rawValue) + options.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.image.rawValue) + if isVideoAllowed { + options.predicate = NSPredicate(format: "mediaType = %d OR mediaType = %d", PHAssetMediaType.image.rawValue, PHAssetMediaType.video.rawValue) + } + assetFetchOptions = [ .smartAlbum: options, .album: options, diff --git a/AssetsPickerViewController/Classes/Picker/Controller/AssetsPickerViewController.swift b/AssetsPickerViewController/Classes/Picker/Controller/AssetsPickerViewController.swift index 5717479..ff1ca91 100644 --- a/AssetsPickerViewController/Classes/Picker/Controller/AssetsPickerViewController.swift +++ b/AssetsPickerViewController/Classes/Picker/Controller/AssetsPickerViewController.swift @@ -22,6 +22,7 @@ import Photos } // MARK: - AssetsPickerViewController +@objcMembers open class AssetsPickerViewController: UINavigationController { @objc open weak var pickerDelegate: AssetsPickerViewControllerDelegate? From 5e9c5042d470fa8620280e21457cb76b52bd1b81 Mon Sep 17 00:00:00 2001 From: Michael Kushinski Date: Mon, 7 Mar 2022 19:54:23 -0500 Subject: [PATCH 20/20] Fix for crash when requesting permission on iOS 15.2 --- .../Classes/Photo/Controller/AssetsPhotoViewController.swift | 1 + .../Classes/Picker/Controller/AssetsPickerViewController.swift | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController.swift b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController.swift index 2c96cbc..4f60673 100644 --- a/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController.swift +++ b/AssetsPickerViewController/Classes/Photo/Controller/AssetsPhotoViewController.swift @@ -156,6 +156,7 @@ open class AssetsPhotoViewController: UIViewController { guard let `self` = self else { return } self.updateNoPermissionView() if isGranted { + AssetsManager.shared.registerObserver() self.setupAssets() } else { self.delegate?.assetsPickerCannotAccessPhotoLibrary?(controller: self.picker) diff --git a/AssetsPickerViewController/Classes/Picker/Controller/AssetsPickerViewController.swift b/AssetsPickerViewController/Classes/Picker/Controller/AssetsPickerViewController.swift index 5717479..af0871d 100644 --- a/AssetsPickerViewController/Classes/Picker/Controller/AssetsPickerViewController.swift +++ b/AssetsPickerViewController/Classes/Picker/Controller/AssetsPickerViewController.swift @@ -68,7 +68,6 @@ open class AssetsPickerViewController: UINavigationController { controller.pickerConfig = config self.photoViewController = controller - AssetsManager.shared.registerObserver() viewControllers = [photoViewController] }