2017年3月27日月曜日

カメラロールのようなシンプルなイメージピッカーの作成

このエントリーをはてなブックマークに追加

カメラロールのようなシンプルなイメージピッカーの作成

概要

カメラロールのようなシンプルなイメージピッカーを作る方法について説明します。 CollectionViewを用意し、PHFetchResultで取得したPHAssetから画像を取得し、表示します。 また、iOS10より導入されたCollectionViewのprefetchを利用します。

screen_shot

ImagePickerViewControllerの説明

クラス定義

各種定数の定義と変数の初期化をします。kColumnCntやkCellSpacingの値を変えることで、CollectionViewの見た目を変更できます。 targetSizeはCollectionViewCellの大きさであり、後から読み込む画像サイズの大きさにもなります。(initView内で計算するため、CGSize.zeroを仮に代入する。)

class ImagePickerViewController: UIViewController {
    @IBOutlet fileprivate weak var collectionView: UICollectionView!

    fileprivate let kCellReuseIdentifier = "Cell"
    fileprivate let kColumnCnt: Int = 3
    fileprivate let kCellSpacing: CGFloat = 2
    fileprivate var fetchResult: PHFetchResult<PHAsset>!
    fileprivate var imageManager = PHCachingImageManager()
    fileprivate var targetSize = CGSize.zero

    override func viewDidLoad() {
        super.viewDidLoad()

        initView()
        loadPhotos()
    }
}

ビューの初期化と写真の読み込み

カラム数とマージンサイズ、CollectionViewのwidthよりtargetSizeを決定します。 また、loadPhotos内では、PHFetchOptionsにより写真を作成日時の降順に読み込みます。 なお、CollectionViewのdelegateとdataSourceへの紐づけは、Interface Builder上で行っています。

fileprivate extension ImagePickerViewController {
    fileprivate func initView() {
        let imgWidth = (collectionView.frame.width - (kCellSpacing * (CGFloat(kColumnCnt) - 1))) / CGFloat(kColumnCnt)
        targetSize = CGSize(width: imgWidth, height: imgWidth)

        let layout = UICollectionViewFlowLayout()
        layout.itemSize = targetSize
        layout.minimumInteritemSpacing = kCellSpacing
        layout.minimumLineSpacing = kCellSpacing
        collectionView.collectionViewLayout = layout

        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: kCellReuseIdentifier)
    }

    fileprivate func loadPhotos() {
        let options = PHFetchOptions()
        options.sortDescriptors = [
            NSSortDescriptor(key: "creationDate", ascending: false)
        ]
        fetchResult = PHAsset.fetchAssets(with: .image, options: options)
    }
}

UICollectionViewDataSourceの実装

UICollectionViewDataSourceを実装します。fetchResult.object(at: indexPath.item)により、PhotoAssetオブジェクトを取得し、その後requestImageによりUIImageを取得します。

extension ImagePickerViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kCellReuseIdentifier, for: indexPath)
        let photoAsset = fetchResult.object(at: indexPath.item)
        imageManager.requestImage(for: photoAsset, targetSize: targetSize, contentMode: .aspectFill, options: nil) { (image, info) -> Void in
            let imageView = UIImageView(image: image)
            imageView.frame.size = cell.frame.size
            imageView.contentMode = .scaleAspectFill
            imageView.clipsToBounds = true
            cell.contentView.addSubview(imageView)
        }
        return cell
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return fetchResult.count
    }
}

UICollectionViewDelegateの実装

UICollectionViewDelegateを実装し、Cellのサイズやセクション数を決定します。 また、didSelectItemAtで写真選択時の処理を記述します。

extension ImagePickerViewController: UICollectionViewDelegate {
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }

    func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize {
        return targetSize
    }

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let photoAsset = fetchResult.object(at: indexPath.item)
        print(photoAsset.description)
    }
}

UICollectionViewDataSourcePrefetchingの実装

iOS10より追加されたUICollectionViewDataSourcePrefetchingを実装します。 prefetchItemsAtで写真をキャッシュし、cancelPrefetchingForItemsAtでキャッシュを開放します。

extension ImagePickerViewController: UICollectionViewDataSourcePrefetching {
    func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
        DispatchQueue.main.async {
            self.imageManager.startCachingImages(for: indexPaths.map{ self.fetchResult.object(at: $0.item) }, targetSize: self.targetSize, contentMode: .aspectFill, options: nil)
        }
    }

    func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
        DispatchQueue.main.async {
            self.imageManager.stopCachingImages(for: indexPaths.map{ self.fetchResult.object(at: $0.item) }, targetSize: self.targetSize, contentMode: .aspectFill, options: nil)
        }
    }
}

サンプル

Image-Picker@githubに動作するプロジェクトがあります。

動作確認

このTipsは、「スマホの写真素材が売買できるサイトSnapmart」を開発する中で生まれました。 実際の動作をSnapmartアプリ(iOS)から確認できますので、是非ダウンロードしてみてください!