こんにちは、モバイルチームの松元(@daikimat)です。
iOSアプリを作る上で画像選択のUIを作るということはよくあることと思います。そんな時に使える汎用的なライブラリを作成しました。この記事では今回作成したライブラリの紹介と、画像選択UIを実装する上で必要なPhotoKit / Photos frameworkの基礎的な知識やハマりポイントを紹介します。
ライブラリを作った背景
昨年、モバイルでのあるユーザーシナリオを検証するためプロトタイプを作り一部のユーザー様とベータテストを行ないました。 その中で「画像をサクサク撮って添付したい、既にフォトライブラリに保存されている画像もサクサク添付したい」といったシナリオがあり、カメラ撮影とフォトライブラリの画像の選択を手軽に行えるiMessageのUIを参考に実装しました。 そのプロトタイプはリリースには至りませんでしたが、この画像選択UI部分は汎用的ということで昨年行われたハッカソンで汎用化 + ライブラリ化し社内限定で使えるようにしていました。 せっかくなので今回社外にも公開し、実装時にたまった知見も共有することにしました。ちなみに参考にしたiMessageの画像添付UIはiOS12からiMessage Appと一体化して今は無きUIとなりました。
AttachmentInput
画像選択UIライブラリは ↓で公開しています。
github.com
UIResponder
のinputView
に当ててキーボードとして使うことを想定しています。
iOS11のiMessageのUIにそっくりですが、iMessageと違う点でいうと画像や動画の圧縮やファイル名、ファイルサイズの取得にも対応しています。
他にもやりたいことはissuesに追加しています。至らない点が多々あると思います。英文がおかしいとかリファクタリングでも良いのでPRをお待ちしています。
Tips集
ここからがタイトルに書いた本題ですが、上記ライブラリを作るにあたってPhotos frameworkに関して情報少ないなー、辛かった気がするなーという点をこちらに列挙しておきますのでどなたかのお役に立てれば幸いです。
画像と動画のみの一覧を取得する
画像を取得するにはPhotoKitのPhotos frameworkを使って画像や動画などを表すPHAssetを取得することになります。
PHAssetの種類であるPHAssetMediaTypeによるとimage
とvideo
の他にaudio
とunknown
タイプがあって、ここでは不要なのでPHFetchOptionsで取得しないように指定します。他にもアセットの取得数の上限や、ソート方法など指定することができます。
let fetchOptions = PHFetchOptions() fetchOptions.fetchLimit = 100 // 一度に取得するアセットの数 fetchOptions.predicate = NSPredicate(format: "mediaType == %d || mediaType == %d", PHAssetMediaType.image.rawValue, PHAssetMediaType.video.rawValue) // imageとvideoのみ取得 fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] // 作成日時で降順でソート let fetchResult = PHAsset.fetchAssets(with: fetchOptions)
端末に保存されている画像の変更を反映する
端末に画像が追加/削除されたり変更が加えられた場合、それらの変更を検知したい場合はPHPhotoLibraryChangeObserver
を用いることで取得できます。
class ReceiverClass: NSObject { var fetchResult: PHFetchResult<PHAsset>? = nil func yourInitialization() { PHPhotoLibrary.shared().register(self) } func fetch() { self.fetchResult = PHAsset.fetchAssets(with: PHFetchOptions()) } } extension ReceiverClass: PHPhotoLibraryChangeObserver { func photoLibraryDidChange(_ changeInstance: PHChange) { if let fetchResult = self.fetchResult, let changeDetails = changeInstance.changeDetails(for:fetchResult) { let newFetchResul = changeDetails.fetchResultAfterChanges } } }
AttachmentInputでの使用例 register、PHPhotoLibraryChangeObserver
iCloudに保存されている画像を取得する
iCloudに保存されている画像はPHImageRequestOptions
でisNetworkAccessAllowed
をtrue
にすると取得できます。
let imageRequestOptions = PHImageRequestOptions() imageRequestOptions.isSynchronous = false imageRequestOptions.isNetworkAccessAllowed = true imageRequestOptions.progressHandler = { (progress, error, stop, info) in if let error = error { // エラー return } // progress 0.0 ~ 1.0 でダウンロードの進捗を確認できます。 } imageManager.requestImageData(for: phasset, options: imageRequestOptions){ (imageData, _, _, _) in if let imageData = imageData { // 画像取得 } }
PHAssetからファイル名、ファイルサイズ、ファイルの保存されているパスを取得する
ファイル名を取得するには PHAssetResource.assetResources(for: phAsset)
でPHAssetResource
を取得することで.originalFilename
から取得することが可能です。
非公式の方法ですが、resource.value(forKey: "fileSize")
でファイルサイズ、resource.value(forKey: "fileURL")
でファイルパスを取得できます。
PHAssetResource.assetResources(for: phAsset)
は処理が重いため必要なassetに対してのみ非同期で行うことをお勧めします。
※ valueで情報を取得する方法はPHAssetResourceの公開仕様ではないので、今後も使えるかは保証できません。
※ PHAsset#requestContentEditingInput(with:completionHandler:)
でも情報を取得することもできますが、
画像編集のセッションを開始するため情報を取得するだけにしてはメモリも食うし扱いにくいです。
DispatchQueue.global().async { let resources = PHAssetResource.assetResources(for: phAsset) if let resource = resources.first { let fileName = resource.originalFilename let unsignedInt64 = resource.value(forKey: "fileSize") as? CLong let sizeOnDisk = Int64(bitPattern: UInt64(unsignedInt64!)) let fileURL = resource.value(forKey: "fileURL") as? URL } }
AttachmentInputでの使用例
AttachmentInputでは使っていないですが、iCloudに保存されていて取得にネットワークが必要な場合はlocallyAvailable
をチェックすると判定できます。
こちらも非公式の方法なので今後も使えるかは保証できません。
resource.value(forKey: "locallyAvailable")
PHImageManagerのオプションの組み合わせによる挙動の違い
PHImageManagerのオプションで挙動を理解できずに一度は苦しむresizeMode
とdeliveryMode
があります。
複数回段階的にロードされたり取得される画像のサイズが指定したものと違うものが返ってきたり、この二つのオプションの組み合わせによって直感では理解しがたい動きをします。
ドキュメントを読んでも細かいサイズの違いなどは読み取ることが難しいです。こちらは先人の知恵で素晴らしいマトリクスが公開されていましたので転載させていただきます。
ImagePickerControllerからphAssetを取得する
ImagePickerCellDelegate
のimagePickerController(_:didFinishPickingMediaWithInfo:)でImagePickerから画像や動画を選択した際の情報を取得できます。ここからphAssetを得るにはiOS11以降とそれより前で実装が変わります。
iOS11以降はinfo[.phAsset]で一発で取得できます。iOS10以下の場合はinfo[.referenceURL](Deprecated)からURLが取得できるので、fetchAssets(withALAssetURLs:options:)で取得することができます。
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { var phAsset: PHAsset? = nil if #available(iOS 11.0, *) { phAsset = info[.phAsset] as? PHAsset } else { if let url = info[.referenceURL] as? URL { let fetchResult = PHAsset.fetchAssets(withALAssetURLs: [url], options: nil) phAsset = fetchResult.firstObject } } }
現場からは以上です!良いPhotosライフを!