[Photos][Combine] requestAuthorization を Combine と組み合わせて使う

Combine を使うと、completion handler ベースになっている API を reactive 的に使うことが簡単にできますので、説明します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.3
  • Xcode 12.5
  • iOS 14.5

Photos の requestAuthorization

写真ライブラリへのアクセスをユーザーに確認するための API として用意されています。


class func requestAuthorization(for accessLevel: PHAccessLevel, 
                        handler: @escaping (PHAuthorizationStatus) -> Void)

Handler の引数を見るとわかるように、completion handler を渡して、処理終了時に処理できるようになっています。

requestAuthorization の使い方

普通に使うと、以下のようなコードになります。


    PHPhotoLibrary.requestAuthorization(for: accessLevel) { (status) in
        if status == .authorized {
            // authorized to access
            // start to get photo data
        } else {
            // rejected....
            return
        }
    }

1つなら、ネストも1段ですのでまだ良いのですが、この handler の中で、さらに completion handler を使う API を呼び始めると、ネストが深くなっていき、読むのが難しいコードになっていきます。

Combine

SwiftUI と同時期に導入された Combine を使うことで、declarative に記述していくことができます。

Combine の提供する Publisher には、何度もイベントを送信することを前提とする Publisher も多いのですが、一度の送信で終了となるような Publisher も用意されています。

Future という Publisher は、一度送信すると、終了します。


final class Future where Failure : Error

requestAuthorization も、一度結果を取得すると処理としては終了ですので、Future を使うのがちょうど良さそうです。

Future と requestAuthorization

Future を使って、requestAuthorization の呼び出しを wrap すると以下のようになります。


    func authorizationPublisher(_ accessLevel: PHAccessLevel ) -> Future {
        Future{ promise in
            print("start to check")
            if PHPhotoLibrary.authorizationStatus(for: accessLevel) == .authorized {
                return promise(.success(true))
            }
            PHPhotoLibrary.requestAuthorization(for: accessLevel) { (status) in
                switch status {
                    case .authorized, .limited:
                        return promise(.success(true))
                    case .denied, .notDetermined, .restricted:
                        return promise(.success(false))
                    @unknown default:
                        return promise(.failure(MyAppPHError.unknownAuthStatus))
                }
            }
        }
    }

こうすることで、requestAuthorization を呼び出す側は以下のようになります。


    self.authorizationPublisher(.readWrite)
        .sink(receiveCompletion: { (completion) in
            switch completion {
                case .finished:
                    break
                case .failure(let error):
                    print(error)
            }
        }, receiveValue: { (status) in
            if !status { return } // denied
            // start to get photo
        })
        .store(in: &cancellables)

Deffered

Future を使って定義すると、その closure は、Future が生成されたタイミングで実行されてしまいます。

ケースによっては、不必要な処理が走ってしまうこともあります。

実際に、subscribe された時に処理されるものとして、Deffered という publisher も用意されています。

以下のように、Deffered で Future を wrap すると、実際に subscribe したタイミングで 実行されるようになります。


    func authorizationPublisher(_ accessLevel: PHAccessLevel ) -> Deferred> {
        Deferred {
            Future{ promise in
                print("start to check")
                if PHPhotoLibrary.authorizationStatus(for: accessLevel) == .authorized {
                    return promise(.success(true))
                }
                PHPhotoLibrary.requestAuthorization(for: accessLevel) { (status) in
                    switch status {
                        case .authorized, .limited:
                            return promise(.success(true))
                        case .denied, .notDetermined, .restricted:
                            return promise(.success(false))
                        @unknown default:
                            return promise(.failure(MyAppPHError.unknownAuthStatus))
                    }
                }
            }
        }
    }

上記のような Publisher を作っておくと、アクセスが必要な箇所で、Publisher からの subscribe として処理を書いておくと、requestAuthorization せずにアクセスしようとしてしまう不具合を防止することができます。

まとめ:Future/Deffered と requestAuthorization を組み合わせて使う

Future/Deffered と requestAuthorization を組み合わせて使う
  • PHPhotoLibrary の requestAuthorization は、Future/Deffered と組み合わせることで、declarative に使用できる
  • Future だけだと、インスタンス生成時に処理されてしまうが、Deffered をつけると subscribe 時の処理となる

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です