###### tags: `第14屆IT邦鐵人賽文章` # 【在 iOS 開發路上的大小事2-Day13】PhotoKit 好像很好玩 (2) 在上一篇,有講到 PhotoKit 的 PHAccessLevel、PHAuthorizationStatus 在這一篇,會講到 PHFetchOptions、PHPhotoLibraryChangeObserver、PHImageManager ## PHFetchOptions PHFetchOptions 是用來取得手機照片時,設定要取得哪邊的照片、要如何排序、要顯示哪些照片等的 那 PHFetchOptions 裡面有哪些東西呢~下面來一一介紹 ```swift @available(iOS 8, *) open class PHFetchOptions : NSObject, NSCopying { // 照片篩選條件 @available(iOS 8, *) open var predicate: NSPredicate? // 照片排序方式 @available(iOS 8, *) open var sortDescriptors: [NSSortDescriptor]? // 在抓取的照片中,是否要包含隱藏的照片,預設為 false @available(iOS 8, *) open var includeHiddenAssets: Bool // 在抓取的照片中,是否要包含連拍的照片,預設為 false @available(iOS 8, *) open var includeAllBurstAssets: Bool // 在抓取的照片中,照片的來源,預設為 PHAssetSourceTypeNone @available(iOS 9, *) open var includeAssetSourceTypes: PHAssetSourceType // 抓取照片的數量限制,預設為 0 (0 = 無限制) @available(iOS 9, *) open var fetchLimit: Int // 用於確認 App 是否接收到獲取結果中對象的詳細更改資訊,預設為 true @available(iOS 8, *) open var wantsIncrementalChangeDetails: Bool } ``` ### PHAssetSourceType 上面有出現一個 ```PHAssetSourceType```,這個是照片的來源,一共有三種,分別為 ```swift @available(iOS 9, iOS 8, *) public struct PHAssetSourceType : OptionSet { // 使用者本地的照片 @available(iOS 8, *) public static var typeUserLibrary: PHAssetSourceType { get } // 使用者 iCloud 共享的照片 @available(iOS 8, *) public static var typeCloudShared: PHAssetSourceType { get } // 使用者 iTunes Sync 的照片 @available(iOS 8, *) public static var typeiTunesSynced: PHAssetSourceType { get } } ``` ### 取得篩選的照片 透過 PHFetchOptions 可以排序、篩選出照片 那要如何排序跟篩選呢?可以參考下面的 Sample Code ```swift // 排序 allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)] // 排序 + 篩選 let defaultPredicate = "self.mediaType==1 OR self.mediaType==2 OR self.mediaSubtypes==8 OR self.isFavorite==true OR self.isFavorite==false" allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)] allPhotosOptions.predicate = NSPredicate(format: defaultPredicate) ``` 排序跟篩選完之後,就要來取得了,取得方法也很簡單 可以參考下面的 Sample Code ```swift let defaultPredicate = "self.mediaType==1 OR self.mediaType==2 OR self.mediaSubtypes==8 OR self.isFavorite==true OR self.isFavorite==false" allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)] allPhotosOptions.predicate = NSPredicate(format: defaultPredicate) allPhotos = PHAsset.fetchAssets(with: allPhotosOptions) ``` ## PHPhotoLibraryChangeObserver 手機內的照片,隨時都有可能會改變,既然有改變,那就需要更新狀態 那要如何更新狀態呢?透過註冊相簿監聽,就可以即時取得最新的相簿狀態了 ```swift PHPhotoLibrary.shared().register(self) // 註冊相簿變化的觀察 ``` 接著要繼承 ```PHPhotoLibraryChangeObserver``` 這個 Protocol 並實作 可以參考下面的 Sample Code ```swift extension PhotosViewController: PHPhotoLibraryChangeObserver { func photoLibraryDidChange(_ changeInstance: PHChange) { // 當相簿發生變化時,要做對應的 UI 處理 guard let changes = changeInstance.changeDetails(for: allPhotos) else { return } DispatchQueue.main.async { self.allPhotos = changes.fetchResultAfterChanges if (changes.hasIncrementalChanges) { guard let collectionView = self.photosCollectionView else { fatalError() } collectionView.performBatchUpdates({ if let removed = changes.removedIndexes, removed.count > 0 { collectionView.deleteItems(at: removed.map({ IndexPath(item: $0, section: 0) })) } if let inserted = changes.insertedIndexes, inserted.count > 0 { collectionView.insertItems(at: inserted.map({ IndexPath(item: $0, section: 0) })) } if let changed = changes.changedIndexes, changed.count > 0 { collectionView.reloadItems(at: changed.map({ IndexPath(item: $0, section: 0) })) } changes.enumerateMoves { fromIndex, toIndex in collectionView.moveItem(at: IndexPath(item: fromIndex, section: 0), to: IndexPath(item: toIndex, section: 0)) } }, completion: nil) } else { self.photosCollectionView.reloadItems(at: [self.itemIndexPath]) } } } } ``` ## Sample UI Design 這邊我是以 UICollectionView 來顯示類似照片牆的畫面 ![Sample UI Design](https://i.imgur.com/9Tp8oDx.jpg =293x633) CollectionViewCell 的 UI Sample Code 如下 ```swift class PhotosCollectionViewCell: UICollectionViewCell { @IBOutlet weak var photosImage: UIImageView! @IBOutlet weak var favoriteImage: UIButton! @IBOutlet weak var typeImage: UIImageView! static let identifier = "PhotosCollectionViewCell" var representedAssetIdentifier: String = "" var smallImage: UIImage! { didSet { photosImage.image = smallImage } } var sourceImage: UIImage! { didSet { typeImage.image = sourceImage typeImage.tintColor = .white } } var heartImage: String! { didSet { favoriteImage.setTitle(heartImage, for: .normal) favoriteImage.tintColor = .white } } override func awakeFromNib() { super.awakeFromNib() } } ``` CollectionView 的 cellForItemAt Sample Code 如下 ```swift func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let asset = allPhotos.object(at: indexPath.item) itemIndexPath = indexPath guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotosCollectionViewCell.identifier, for: indexPath) as? PhotosCollectionViewCell else { fatalError("Can't Load Photos CollectionView Cell!") } cell.representedAssetIdentifier = asset.localIdentifier photoCacheImageManager.requestImage(for: asset, targetSize: thumbnailSize, contentMode: .default, options: nil) { image, _ in if (cell.representedAssetIdentifier == asset.localIdentifier) { cell.smallImage = image cell.heartImage = asset.isFavorite ? "♥︎" : "" if (asset.mediaSubtypes == .photoLive) { cell.sourceImage = UIImage(systemName: "livephoto") } else if (asset.mediaType == .image) { cell.sourceImage = UIImage(systemName: "photo") } else if (asset.mediaType == .video) { cell.sourceImage = UIImage(systemName: "video") } } } return cell } ``` ## PHImageManager 在上面這段 Sample Code 中,有幾個可以注意的地方 ```swift // 1 let asset = allPhotos.object(at: indexPath.item) // 2 asset.localIdentifier // 3 photoCacheImageManager.requestImage(for: asset, targetSize: thumbnailSize, contentMode: .default, options: nil) ``` #### 首先是第一個 由於 allPhotos 的型別是 PHFetchResult<PHAsset>,所以會回傳有序、類似陣列的東西 因此我們可以透過 ```.object(at:)``` 的方式,來取得對應的照片 #### 再來是第二個 每個 PHAsset 都會有對應的 localIdentifier,來做為表示,有點像是 UUID 的感覺 #### 最後是第三個 透過 PHImageManager 可以請求照片、原況照片、影片的方式,Function 如下 ```swift // 用來請求照片 @available(iOS 8, *) open func requestImage(for asset: PHAsset, targetSize: CGSize, contentMode: PHImageContentMode, options: PHImageRequestOptions?, resultHandler: @escaping (UIImage?, [AnyHashable : Any]?) -> Void) -> PHImageRequestID // 用來請求原況照片 @available(iOS 9.1, *) open func requestLivePhoto(for asset: PHAsset, targetSize: CGSize, contentMode: PHImageContentMode, options: PHLivePhotoRequestOptions?, resultHandler: @escaping (PHLivePhoto?, [AnyHashable : Any]?) -> Void) -> PHImageRequestID // 用來請求要播放的影片 (只能回放) @available(iOS 8, *) open func requestPlayerItem(forVideo asset: PHAsset, options: PHVideoRequestOptions?, resultHandler: @escaping (AVPlayerItem?, [AnyHashable : Any]?) -> Void) -> PHImageRequestID // 用來請求要匯出的影片 @available(iOS 8, *) open func requestExportSession(forVideo asset: PHAsset, options: PHVideoRequestOptions?, exportPreset: String, resultHandler: @escaping (AVAssetExportSession?, [AnyHashable : Any]?) -> Void) -> PHImageRequestID // 用來請求要播放的影片 @available(iOS 8, *) open func requestAVAsset(forVideo asset: PHAsset, options: PHVideoRequestOptions?, resultHandler: @escaping (AVAsset?, AVAudioMix?, [AnyHashable : Any]?) -> Void) -> PHImageRequestID ``` ```swift asset: PHAsset // 你要請求的照片資源 targetSize: CGSize // 你要請求的照片尺寸大小 contentMode: PHImageContentMode // 你要請求的照片顯示模式 options: PHImageRequestOptions? // 你要請求的照片選項 ``` ### PHImageContentMode 照片顯示模式 ```PHImageContentMode``` 一共有三種 ```swift @available(iOS 8, iOS 8, *) public enum PHImageContentMode : Int { @available(iOS 8, *) case aspectFit = 0 @available(iOS 8, *) case aspectFill = 1 @available(iOS 8, *) public static var `default`: PHImageContentMode { get } } ``` ### PHImageRequestOptions 照片請求選項 ```PHImageRequestOptions```,裡面有許多選項可以設定 ```swift @available(iOS 8, *) open class PHImageRequestOptions : NSObject, NSCopying { // 照片的版本 @available(iOS 8, *) open var version: PHImageRequestOptionsVersion // 照片顯示的畫質版本,預設為 PHImageRequestOptionsDeliveryModeOpportunistic @available(iOS 8, *) open var deliveryMode: PHImageRequestOptionsDeliveryMode // 重新設定的照片大小,預設為 PHImageRequestOptionsResizeModeFast @available(iOS 8, *) open var resizeMode: PHImageRequestOptionsResizeMode // 是否對原始照片進行裁切,預設為 CGRectZero (不裁切) // 要裁切的話 resizeMode 需設為 PHImageRequestOptionsResizeMode.exact @available(iOS 8, *) open var normalizedCropRect: CGRect // 是否下載 iCloud 上的照片 @available(iOS 8, *) open var isNetworkAccessAllowed: Bool // 是否同步處理一個照片請求,預設為 false @available(iOS 8, *) open var isSynchronous: Bool // 下載 iCloud 照片的進度處理管理者 @available(iOS 8, *) open var progressHandler: PHAssetImageProgressHandler? } ``` 下一篇,再來繼續介紹 PHAsset~ ## 參考資料 > https://developer.apple.com/documentation/photokit > https://www.jianshu.com/p/0ff787121ebc > https://www.jianshu.com/p/78960c4fd99d > https://foolish-boy.github.io/2017/%E8%81%8A%E8%81%8AALAssetsLibrary%E4%B8%8EPhotos/ > https://juejin.cn/post/6985128108965756936 > https://www.csdn.net/tags/MtTaAg2sNzU5NTk2LWJsb2cO0O0O.html > https://www.jianshu.com/p/3f8627d990f3