iOS
UIKit
Swift
UIImagePickerController
RxSwift

iOS で RxSwift を使ってカメラかフォトライブラリから画像を一枚取得する

やったこと

iOS でフォトライブラリ or カメラから画像を一枚取得する機能をUIImagePickerControllerと RxSwift を使って実装したので晒してみます。複数箇所で楽に使えることを目指しました。

環境

Swift 3.1

処理のだいたいの流れ

  1. フォトライブラリ or カメラへのアクセス許可を確認。なければ求める
  2. 許可があればUIImagePickerControllerをモーダル表示
  3. 画像が選択されたらUIImagePickerControllerを閉じて画像をUIImageで取得

実装

  • UIImagePickerController表示 -> 画像選択 -> UIImagePickerControllerを閉じるまでを行ってくれるクラス(ImageManager)を作成し、使う側はpick(on:sourceType:)を呼ぶこととしました。
  • UIImagePickerControllerの画像取得イベントは delegate として渡すImagePickerControllerDelegateクラスにPublishSubjectを持たせることで画像の取得をストリームとして受け取ります。
  • アクセス権限確認まわりのソースが長くなって見にくかったのでメソッドを分けています。
  • 画像の取得に失敗した際には独自に定義したInternalErrorクラスからエラー内容を把握します。InternalErrorの実装は割愛しますがErrorに準拠したただの Enum です。
  • アクセス不許可等でストリームにエラーが流れた際にストリームが止まってしまうので、エラーを流さずResult型を使うことでそれを回避しました。.catchErrorを使用側で使うことも考えましたがこちらのほうが使用側のソースは短く済みそうです。

Result型には antitypical/Result を使用しました。

ImageManager.swift
final class ImageManager {

    // 権限確認 -> 選択された画像のストリーム
    func pick(on viewController: UIViewController, sourceType: UIImagePickerControllerSourceType) -> Observable<Result<UIImage, InternalError>> {
        return self.authorizedSourceType(sourceType)
            // 画像選択画面作成
            .map { sourceType -> (picker: UIImagePickerController, delegate: ImagePickerControllerDelegate) in
            // UIImagePickerControllerとそれにわたすdelegateを生成
                let picker = UIImagePickerController()
                let delegate = ImagePickerControllerDelegate()
                picker.delegate = delegate
                picker.allowsEditing = false
                picker.sourceType = sourceType
                return (picker, delegate)
            }
            .subscribeOn(MainScheduler.instance)
            // 表示
            .do(onNext: { [weak viewController] (picker, _) in 
                viewController?.present(picker, animated: true) 
            })
            // 選択された画像のストリームをResultで
            .flatMap { (picker, delegate) -> Observable<Result<UIImage, InternalError>> in
                return delegate.pickedResultSubject
                    .do(onNext: { _ in picker.dismiss(animated: true) })
                    .map { result -> Result<UIImage, InternalError> in
                        switch result {
                        case .success(let image):
                            return .success(image)
                        case .failure(let error):
                            return .failure(error)
                        }
                }
            }

    }

    // 承認された画像ソースタイプのストリーム
    // 長いので別ストリームとして切り出した
    private func authorizedSourceType(_ sourceType: UIImagePickerControllerSourceType) -> Observable<UIImagePickerControllerSourceType> {
        return Observable<UIImagePickerControllerSourceType>.create { observer -> Disposable in
            switch sourceType {
            case .photoLibrary:
                // フォトライブラリ
                let status = PHPhotoLibrary.authorizationStatus()
                switch status {
                case .authorized:
                    // 承認されていればそのまま返す
                    observer.onNext(.photoLibrary)
                    observer.onCompleted()
                case .denied:
                    // 拒否
                    observer.onError(InternalError.photoLibraryAccessDenied)
                case .notDetermined:
                    // 未承認 許可を求める
                    PHPhotoLibrary.requestAuthorization { status in
                        if .authorized == status {
                            observer.onNext(.photoLibrary)
                            observer.onCompleted()
                        } else {
                            observer.onError(InternalError.photoLibraryAccessDenied)
                        }
                    }
                case .restricted:
                    // アクセス許可制限
                    observer.onError(InternalError.photoLibraryAccessDenied)
                }
            case .camera:
                // カメラ
                let status = AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo)
                switch status {
                case .authorized:
                    // 承認
                    observer.onNext(.camera)
                    observer.onCompleted()
                case .denied:
                    // 拒否
                    observer.onError(InternalError.cameraAccessDenied)
                case .notDetermined:
                    // 未承認 許可を求める
                    AVCaptureDevice.requestAccess(forMediaType: AVMediaTypeVideo) { authorized in
                        if authorized {
                            observer.onNext(.camera)
                            observer.onCompleted()
                        } else {
                            observer.onError(InternalError.cameraAccessDenied)
                        }
                    }
                case .restricted:
                    // アクセス許可制限
                    observer.onError(InternalError.cameraAccessDenied)
                }
            case .savedPhotosAlbum:
                // 実装しなかったので割愛。 .photoLibrary と同じようにできるはず
                break
            }

            return Disposables.create {
                observer.onCompleted()
            }
        }
    }
}

// UIImagePickerControllerに渡すデリゲート
fileprivate final class ImagePickerControllerDelegate: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

    // 選択された画像のストリーム
    let pickedResultSubject = PublishSubject<UIImage>()

    // MARK: - UIImagePickerControllerDelegate

    // 画像が選択された
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
        // 自身のsubjectにイベントを流す
        if let image = info[UIImagePickerControllerOriginalImage] as? UIImage {
            self.pickedResultSubject.onNext(image)
        } else {
            self.pickedResultSubject.onError(InternalError.couldNotPickImage)
        }
    }

}

いざ使ってみる

ボタンのタップでUIImagePickerControllerに渡すUIImagePickerControllerSourceTypeを取得してます。

SomeViewController.swift
// ImageManagerを初期化しておく
private let imageManager = ImageManager()
private let bag = DisposeBag()

override func viewDidLoad() {
    super.viewDidLoad()

    // UIImagePickerControllerSourceTypeを取得
    let sourceTypeStream = Observable<UIImagePickerControllerSourceType>.of(
        self.cameraButton.rx.tap.asObservable().map { .camera }, 
        self.photoLibraryButton.rx.tap.asObservable().map { .photoLibrary }
        )
       .merge()

    sourceTypeStream
        .flatMap { [unowned self] -> Observable<Result<UIImage, InternalError>>
            self.imageManager.pick(on: self, sourceType: sourceType)
        }
        .subscribe(onNext: { [weak self] result in
            switch result {
                case .success(let image): // 画像を取得できた場合の処理
                case .failure(let error): // アクセス不許可等、画像が取得できなかった場合の処理
            }
        })
        .disposed(by: self.bag)
}

さいごに

ソースは割と長くなりましたが、複数箇所で楽に使えるようにはなったかなという感じです。もっと良い実装がありましたら教えてください(特にストリームが止まるのを回避するあたり)。