Edited at

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

More than 1 year has 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)
}



さいごに

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