在上一篇,有講到 PhotoKit 的 PHAccessLevel、PHAuthorizationStatus
在這一篇,會講到 PHFetchOptions、PHPhotoLibraryChangeObserver、PHImageManager
PHFetchOptions 是用來取得手機照片時,設定要取得哪邊的照片、要如何排序、要顯示哪些照片等的
那 PHFetchOptions 裡面有哪些東西呢~下面來一一介紹
@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,這個是照片的來源,一共有三種,分別為
@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
// 排序
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
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)
手機內的照片,隨時都有可能會改變,既然有改變,那就需要更新狀態
那要如何更新狀態呢?透過註冊相簿監聽,就可以即時取得最新的相簿狀態了
PHPhotoLibrary.shared().register(self) // 註冊相簿變化的觀察
接著要繼承 PHPhotoLibraryChangeObserver 這個 Protocol 並實作
可以參考下面的 Sample Code
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])
            }
        }
    }
}
這邊我是以 UICollectionView 來顯示類似照片牆的畫面

CollectionViewCell 的 UI Sample Code 如下
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 如下
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
}
在上面這段 Sample Code 中,有幾個可以注意的地方
// 1
let asset = allPhotos.object(at: indexPath.item)
// 2
asset.localIdentifier
// 3
photoCacheImageManager.requestImage(for: asset, targetSize: thumbnailSize, contentMode: .default, options: nil)
由於 allPhotos 的型別是 PHFetchResult,所以會回傳有序、類似陣列的東西
因此我們可以透過 .object(at:) 的方式,來取得對應的照片
每個 PHAsset 都會有對應的 localIdentifier,來做為表示,有點像是 UUID 的感覺
透過 PHImageManager 可以請求照片、原況照片、影片的方式,Function 如下
// 用來請求照片  
@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
asset: PHAsset // 你要請求的照片資源
targetSize: CGSize // 你要請求的照片尺寸大小
    
contentMode: PHImageContentMode // 你要請求的照片顯示模式
    
options: PHImageRequestOptions? // 你要請求的照片選項
照片顯示模式 PHImageContentMode 一共有三種
@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,裡面有許多選項可以設定
@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