AkkeyLab

Authorization Status thinking in the stream by RxSwift

iOS ではカメラやマイクにアクセスする場合にはユーザに対して許可を貰う必要があります。
まず最初に「アクセスしても良いか」をアラートで訪ねます。そこで拒否されてしまった場合には有効にしてもらうようにユーザに提案し、設定画面に移動させてあげると親切です。今回は、この処理をストリーム思考な設計で実装する事例をご紹介します。

完成形

まずは完成形をみていただきます。ボタンが押されたときにカメラとマイクのアクセス権限を確認して、許可されていないタイプが返ってきたら設定画面に遷移させます。逆に、アクセス権限がすべて許可されていれば、カメラとマイクを使用する処理を実行します。ただし、ここでは確認ダイアログなどの処理は省略しています。 この実装方法に興味を持っていただけたのであれば、記事を読み進めることをお勧めします。

let response = UIButton()
    .rx.tap
    .checkAuthority(requests: [.cameraAccess, .microphoneAccess])
    .share()

response
    .compactMap { $0 }
    .subscribe(onNext: { _ in
        if let url = URL(string: UIApplication.openSettingsURLString) {
            UIApplication.shared.safeOpen(url: url)
        }
    })
    .disposed(by: bag)

response
    .filter { $0 == nil }
    .subscribe(onNext: { _ in
        // Processing with camera and microphone
    })
    .disposed(by: bag)

初回のアクセス権限リクエスト

AVCaptureDevice.authorizationStatus(:) で得られるタイプが notDetermined であった場合は、 AVCaptureDevice.requestAccess(:) を用いて初回のアクセス権限リクエストを行う必要があります。この処理は非同期で行われるため、以下のように RxSwift の処理に合わせます。
以下の処理ではシミュレータやカメラが故障した状態をアクセス拒否と同等に扱っているため、区別することも検討したほうが良いでしょう。

AVCaptureDevice.requestAccess(:) で表示されるアラートは OS 標準のものなので、 Observable を拡張して共通化しました。設定画面に遷移させる処理などは、途中に「設定画面で有効にしてください はい/いいえ」といったダイアログを経由することを想定して共通化しませんでした。導入プロダクトに合わせて臨機応変に対応すると良いでしょう。

extension Observable {
    private func requestAccess(for type: AVMediaType) -> Observable<Bool> {
        Observable<Bool>.create { observer in
            switch AVCaptureDevice.authorizationStatus(for: .audio) {
            case .authorized:
                observer.on(.next(true))
            case .denied, .restricted:
                observer.on(.next(false))
            case .notDetermined:
                AVCaptureDevice.requestAccess(for: type) { isEnabled in
                    observer.on(.next(isEnabled))
                }
            @unknown default:
                observer.on(.next(false))
            }
            return Disposables.create()
        }
    }
}

Observable の拡張

先程のアクセス権限リクエスト処理を用いて、 checkAuthority(:) の処理を実装していきます。カメラとマイクの権限確認を連続して行うために flatMap を用いてリクエスト処理を連結させています。
求めるアクセス権限が1つの場合、複数の場合、すでに許可されている権限がある場合を考慮しているため条件分岐が増えています。

import AVFoundation
import RxSwift

enum Authority {
    case cameraAccess
    case microphoneAccess
}

extension Observable {
    func checkAuthority(requests: [Authority]) -> Observable<(Element, Authority?)> {
        flatMap { arg -> Observable<(Element, Authority?)> in
            for request in requests {
                switch request {
                case .cameraAccess:
                    return self.requestAccess(for: .video)
                        .flatMap { isEnabled -> Observable<(Element, Authority?)> in
                            guard isEnabled && requests.contains(.microphoneAccess) else {
                                return .just(isEnabled ? (arg, nil) : (arg, .cameraAccess))
                            }
                            return self.requestAccess(for: .audio)
                                .map { $0 ? (arg, nil) : (arg, .microphoneAccess) }
                        }
                case .microphoneAccess:
                    return self.requestAccess(for: .audio)
                        .flatMap { isEnabled -> Observable<(Element, Authority?)> in
                            guard isEnabled && requests.contains(.cameraAccess) else {
                                return .just(isEnabled ? (arg, nil) : (arg, .microphoneAccess))
                            }
                            return self.requestAccess(for: .video)
                                .map { $0 ? (arg, nil) : (arg, .cameraAccess) }
                        }
                }
            }
            return .just((arg, nil))
        }
    }
}

実装完了

以上の実装で以下のようなアクセス権限リクエストフローを実現できるようになります。アイデア、ご指摘等ございましたら、このホームページの連絡フォームより気軽に送っていただけると嬉しいです。
最後までお読みいただき、ありがとうございます。

let response = UIButton()
    .rx.tap
    .checkAuthority(requests: [.cameraAccess, .microphoneAccess])
    .share()

response
    .compactMap { $0 }
    .subscribe(onNext: { _ in
        if let url = URL(string: UIApplication.openSettingsURLString) {
            UIApplication.shared.safeOpen(url: url)
        }
    })
    .disposed(by: bag)

response
    .filter { $0 == nil }
    .subscribe(onNext: { _ in
        // Processing with camera and microphone
    })
    .disposed(by: bag)

Author

Next post

Perform full-scale application development with SwiftUI

SwiftUI は WWDC2019 で発表されたフレームワークで、 UI 構築を革新的かつ極めてシンプルに構築することができます。今回は、その SwiftUI で本格的なアプリを開発する上で必要となりえるテクニックをご紹介します。 なお、リリースから約1年ほどしか経過しておらず、今後も大幅なバージョンアップが考えられますので、最新の言語仕様を確認した上で御覧ください。

Read More →