Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

やったこと

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

さいごに

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

KosukeOhmura
Swift, Ruby on Rails とかやってます。
https://www.kosukeohmura.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away