こんにちは。iOSエンジニアの原です。
果物の中では梨がいちばん好きです。
nohanaのiOSアプリの写真選択UIをOSS化して公開しました?
nohana/NohanaImagePicker: A multiple image picker for iOS app.
NohanaImagePickerとは
複数の写真を選択できるピッカーのOSSです。
nohanaの写真選択画面を作り直すことになったので、
せっかくだからということでOSSにしてみました。
nohanaのiOSアプリでは2016年の5月ごろから使っているので、多くのユーザに使われている実績があります。
特徴
オシャレトランジション
写真一覧画面から写真詳細画面に遷移するときに、セルが拡大するようなオシャレトランジションがついてます。
遷移後の画面の操作によって、遷移前の画面の状態が変わっていることがあるので、トランジションをカスタムする時は 行き
よりも 帰り
を丁寧に作る必要があります。
具体的には、写真詳細画面で写真をいっぱい切り替えて、写真一覧画面に戻ろうとすると、いま写真詳細画面で表示している写真が写真一覧画面には表示されていないという状態になります。なので、写真詳細画面から写真一覧画面に戻る直前に写真一覧画面をいいかんじにスクロールしておくという処理をしています。オシャレ〜。
モーメント
iOS標準の写真アプリのモーメントのように、写真一覧を撮影した日付と場所でグルーピングして表示する機能を作りました。
グルーピング自体はiOSが自動的に実行してくれていて、グルーピングしたアルバムの一覧は
class func fetchAssetCollectionsWithType(_ type: PHAssetCollectionType, subtype subtype: PHAssetCollectionSubtype, options options: PHFetchOptions?) -> PHFetchResult
のtypeにPHAssetCollectionType.Moment
を指定すると、取得できます。
ロゴ
OSSにロゴがあるとやる気が出るので、デザイナさんにお願いしたら、かわいいロゴマークを作ってくれました!
以下、デザイナさんコメントです。
- たくさんある写真の中からpick(選択)している様子をアイコンにしてみました。
- 細かい違いですが、色味の調整にもこだわりました!
Swift 3.0
バージョン0.7.1でSwift3.0に対応しました?
使い方
基本的には以下だけで、ピッカーの表示と、選択した写真の情報を取得できます。
import NohanaImagePicker class ViewController: UIViewController, NohanaImagePickerControllerDelegate { override func viewDidAppear(animated: Bool) { super.viewDidAppear(true) let picker = NohanaImagePickerController() picker.delegate = self present(picker, animated: true, completion: nil) } func nohanaImagePickerDidCancel(_ picker: NohanaImagePickerController) { print("?Canceled?") picker.dismiss(animated: true, completion: nil) } func nohanaImagePicker(_ picker: NohanaImagePickerController, didFinishPickingPhotoKitAssets pickedAssts :[PHAsset]) { print("?Completed?\n\tpickedAssets = \(pickedAssts)") picker.dismiss(animated: true, completion: nil) } }
READMEとDemoアプリのコードで解説しているので、詳しくはそちらを見てください。
工夫ポイント
ItemList
アルバム一覧データを格納するクラス(PhotoKitAlbumList) と 写真一覧データを格納するクラス(PhotoKitAssetList) のどちらもが 一覧データを格納するクラス
という特徴を持っているので、その特徴をItemList
(バージョン0.7以前はItemListType
)というプロトコルで表現しました。
ImageListは以下のように定義しています。
public protocol ItemList: Collection { associatedtype Item var title:String { get } func update(_ handler:(() -> Void)?) subscript (index: Int) -> Item { get } }
ImageListがCollectionに準拠しているので、PhotoKitAlbumListとPhotoKitAssetListは少ない実装でCollectionの強力な機能を利用することができています。
またsubscriptも実装しているので、添字で要素にアクセスでき、コードがシンプルになりました。
PhotoKitのラッパーのようなコードですが、写真選択UIのOSSなのにPhotoKitのラッパーを含んでいることは、若干やり過ぎだと思っているので、今後PhotoKitAlbumListとPhotoKitAssetListは別ライブラリにするかもしれません。
画面の状態管理
各画面には、ロード中状態
、ロード完了状態
、空状態(表示するデータがない状態)
、表示するデータがある状態の4つの状態
があるので、それらをprotocolで表現しています。
public protocol EmptyIndicatable { func isEmpty() -> Bool func updateVisibilityOfEmptyIndicator(_ emptyIndicator: UIView) } public protocol ActivityIndicatable { func isProgressing() -> Bool func updateVisibilityOfActivityIndicator(_ activityIndicator: UIView) }
protocol extensionでデフォルトの挙動を実装してあり、インジケータの表示/非表示の切り替え処理をViewController側で書く必要がありません。
public extension ActivityIndicatable where Self: UIViewController { func updateVisibilityOfActivityIndicator(_ activityIndicator: UIView) { if isProgressing() { if !view.subviews.contains(activityIndicator) { view.addSubview(activityIndicator) } } else { activityIndicator.removeFromSuperview() } } }
ロード中状態
と空状態
などが同時に起こりうる作りですが、1画面内だけで完結する状態管理なので今回はこれで十分かなと思います。
開発時に気をつけたこと
用語の統一
以下のように用語を統一しています。
用語 | 意味 | 備考 |
---|---|---|
pick | 写真を選択すること。 | selectはUITableViewControllerやUICollectionViewControllerで使われているので、他のものを考えました。 |
drop | 写真を非選択にすること。 | 同上。 |
asset | 1枚の写真(または動画)のこと。 | PHAssetに対応しています。 |
asset list | 写真の集合のこと。アルバムとも呼びこともできる。 | PHCollectionに対応しています。 |
album list | asset listの集合のこと。アルバム一覧とも呼ぶことができる。 | PHCollectionListに対応しています。 |
nohanaに特化しすぎないこと
nohanaのために書いているコードなので、nohanaに特化して書きたくなってしまうことが多々ありましたが、ぐっとこらえて汎用的に書けるように気をつけました。
おかげで、nohana側のコードで挙動をカスタムする処理が多くなってしまいましたが、逆に言えば挙動をカスタムし易いOSSになったと思います。
※nohanaは画面回転をサポートしていませんが、NohanImagePickerには画面回転にも対応しています(これが意外と大変だった)
まとめ
今後もいろいろと機能追加やリファクタをすすめていきますので、ぜひ使ってみてください。PRやissueも待ってます!
nohana/NohanaImagePicker: A multiple image picker for iOS app.