iT邦幫忙

2022 iThome 鐵人賽

DAY 13
0
Mobile Development

在 iOS 開發路上的大小事2系列 第 13

【在 iOS 開發路上的大小事2-Day13】PhotoKit 好像很好玩 (2)

  • 分享至 

  • xImage
  •  

在上一篇,有講到 PhotoKit 的 PHAccessLevel、PHAuthorizationStatus

在這一篇,會講到 PHFetchOptions、PHPhotoLibraryChangeObserver、PHImageManager

PHFetchOptions

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

上面有出現一個 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)

PHPhotoLibraryChangeObserver

手機內的照片,隨時都有可能會改變,既然有改變,那就需要更新狀態
那要如何更新狀態呢?透過註冊相簿監聽,就可以即時取得最新的相簿狀態了

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])
            }
        }
    }
}

Sample UI Design

這邊我是以 UICollectionView 來顯示類似照片牆的畫面

Sample UI Design

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
}

PHImageManager

在上面這段 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

照片顯示模式 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

照片請求選項 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


上一篇
【在 iOS 開發路上的大小事2-Day12】PhotoKit 好像很好玩 (1)
下一篇
【在 iOS 開發路上的大小事2-Day14】PhotoKit 好像很好玩 (3)
系列文
在 iOS 開發路上的大小事230
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言