###### 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