やったこと
iOS でフォトライブラリ or カメラから画像を一枚取得する機能をUIImagePickerController
と RxSwift を使って実装したので晒してみます。複数箇所で楽に使えることを目指しました。
環境
Swift 3.1
処理のだいたいの流れ
- フォトライブラリ or カメラへのアクセス許可を確認。なければ求める
- 許可があれば
UIImagePickerController
をモーダル表示 - 画像が選択されたら
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)
}
さいごに
ソースは割と長くなりましたが、複数箇所で楽に使えるようにはなったかなという感じです。もっと良い実装がありましたら教えてください(特にストリームが止まるのを回避するあたり)。