ペンギン村 Tech Blog

技術をこよなく愛するエンジニア集団が在住するペンギン村から、世界へ役立つ(かもしれない)技術情報を発信する技術系ブログです。某アラレちゃんが済む村とは一切関係ありません。んちゃ!

Sign in with Apple with Firebase AuthenticationとDelegateをRx化する話

はじめに

お疲れ様です。かむいです。

この記事はiOS Advent Calendar 2019の17日目の投稿となります。
前回は@codelynxさんの名前付き UIView と後付けストアドプロパティもどきでした。

先月Firebase AuthenticationでSign in with Appleが利用できるとの発表がありました。
firebase.googleblog.com

それまでは@fromkkさんが書かれていたようなCloud Functionsを利用したやり方でも近い仕組みを実現できていましたが、やはりFirebase Authentication単体で処理が実現できるのは嬉しい限りです。 qiita.com

今回はこのFirebase Authを利用した実装の紹介だけでなく、実装の中で登場するDelegateメソッドをRx化し、RxSwiftを利用しているケースを想定した値の取り方をやってみようかと思います。
サンプルコードはgithubにあげておりますので、宜しければそちらもご覧ください。

環境

  • Xcode: ver 11.1
  • Swift: ver 5.0
  • CocoaPods: ver 1.8.4
  • Firebase/Authentication: ver 6.13.0

注意

  • サンプルコードは私個人の環境で用意したFirebaseのプロジェクト, Developer Portal上で設定したAppIDの連携により動作検証したものとなります。
    • 直接クローン, ビルドしただけでは動きの確認はできませんのでご注意ください。
  • 2019/12/17現在、Firebase Auth上のApple項目はBeta版となっております。今後仕様に変更があり、その結果この記事の実装だとうまく動作しなくなることがあるかもしれないのでご注意ください。
  • FirebaseをiOSプロジェクトに導入するまでの手順は割愛しています。Firebaseの導入手順については公式ドキュメントにまとまっておりますのでそちらをご参照ください firebase.google.com

サンプルコードの画面構成

ログイン画面とログイン後の画面の2画面構成です。ログイン済みか否かを判定し、結果によって遷移先を変更します。
- 未ログイン: ログイン画面
- ログイン済: ログイン後画面

f:id:kamui_project_tony:20191210222340p:plain

Firebaseの設定

  • 使用したいプロジェクトを選択
  • 画面左の項目から 開発/Authentication を選択

f:id:kamui_project_tony:20191212203814j:plain

  • Authentication画面の ログイン方法 タブを選択
  • ログインプロバイダ 内にある Apple(Beta) の右側の鉛筆アイコンを押下

f:id:kamui_project_tony:20191210222740j:plain

  • 有効にする スイッチをONにし、保存ボタンを押下

f:id:kamui_project_tony:20191210222852j:plain

  • ログインプロバイダApple(Beta) の項目が 有効 に変わったことを確認

f:id:kamui_project_tony:20191210223001p:plain

Developer Portal

  • Certificates, Identifiers & Profiles を選択
  • Identifiers を選択
  • アプリ上で利用しているAppIDを選択
  • Capabilities の項目から Sign in with Apple にチェックをつける
    • Automatically manage signing を使用しXcode上でCapabilitiesを選択した場合(後述)、ここは自動でチェックがつく

f:id:kamui_project_tony:20191210223214p:plain

Xcode

  • TARGETS/アプリのMainTarget を選択し Signing & Capabilities を選択
  • + Capability を選択し Sign in with Apple を選択

f:id:kamui_project_tony:20191210223333j:plain

実装

ログインボタン

f:id:kamui_project_tony:20191210223620p:plain

//
// ViewController.swift
//
@available(iOS 13.0, *)
private func setupSignInWithApple() {
   
    // ASAuthorizationAppleIDButtonの親クラスはUIControlなので注意
    let signInWithAppleButton = ASAuthorizationAppleIDButton(
        authorizationButtonType: .signIn,
        authorizationButtonStyle: .black
    )
    signInWithAppleButton.addTarget(
        self,
        action: #selector(didTapSignInWithApple),
        for: .touchUpInside
    )
    // 公式サンプルに倣いUIStackViewに追加
    stackView.addArrangedSubview(signInWithAppleButton)
}

// MARK: - Action
    
@objc func didTapSignInWithApple() {
    viewModel?.inputs.didTapSignInWithApple(context: self)
}
備考

ログイン処理その1

//
// LoginServiceImpl.swift
//
import AuthenticationService
import CryptoKit

@available(iOS 13.0, *)
private func initAuthorizationController(with context: UIViewController?) -> ASAuthorizationController {
    let appleIDProvider = ASAuthorizationAppleIDProvider()
    let nonce = randomNonceString()
    self.nonce = nonce
    
    let request = appleIDProvider.createRequest()
    request.requestedScopes = [.fullName, .email]

    // ノンス設定
    request.nonce = sha256(nonce)
        
    let authorizationController = ASAuthorizationController(authorizationRequests: [request])
        
    if let controller = context as? ASAuthorizationControllerPresentationContextProviding {
        // ログイン画面上でSignInWithAppleのモーダル表示を行いたいためログイン画面のインスタンスを設定
        // ログイン画面のViewControllerはASAuthorizationControllerPresentationContextProvidingに準拠している
        authorizationController.presentationContextProvider = controller
    }

    // 許可フロー開始
    authorizationController.performRequests()
        
    return authorizationController
}

extension LoginServiceImpl {

    private func randomNonceString(length: Int = 32) -> String {
        precondition(length > 0)
        let charset: Array<Character> =
            Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
        var result = ""
        var remainingLength = length
        
        while remainingLength > 0 {
            let randoms: [UInt8] = (0 ..< 16).map { _ in
                var random: UInt8 = 0
                let errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random)
                if errorCode != errSecSuccess {
                    fatalError("Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)")
                }
                return random
            }
            
            randoms.forEach { random in
                if length == 0 { return }
                if random < charset.count {
                    result.append(charset[Int(random)])
                    remainingLength -= 1
                }
            }
        }
        return result
    }
    
    @available(iOS 13.0, *)
    private func sha256(_ input: String) -> String {
      let inputData = Data(input.utf8)
      let hashedData = SHA256.hash(data: inputData)
      let hashString = hashedData.compactMap {
        return String(format: "%02x", $0)
      }.joined()

      return hashString
    }

}
備考

ログイン処理その2

上記ログイン処理で ASAuthorizationController のインスタンスを作成し許可フローを開始しましたが、その後にコールバックされる処理が以下のDelegateメソッド群です。

extension HogeViewController: ASAuthorizationControllerDelegate {
    func authorizationController(controller _: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        // 成功時の処理
    }

    func authorizationController(controller _: ASAuthorizationController, didCompleteWithError error: Error) {
        // 失敗時の処理
    }
}

私は普段の開発でRxSwiftを利用しているため、できればDelegateメソッドをそのまま使うのではなく、1つの Signinストリーム という非同期処理の括りでSign in with Appleを実現したいと思いました。
サンプルコードでは以下の箇所がそれに当たります。

// 
// LoginServiceImpl.swift
// 
func login(context: UIViewController?) -> Completable {
    let authorizationController = initAuthorizationController(with: context)
                 
    return authorizationController
        .rx
        .didComplete
        .flatMap({ [weak self] (authorization, error) -> Completable in
            if let error = error {
                return .error(error)
            }
            guard
                let self = self,
                let credential = self.initCredential(with: authorization)
            else {
                throw AuthenticationError.failedToCreateCredential
            }
            return self.signIn(with: credential)
        })
        .asCompletable()
    }

authorizationController変数からrxが生え、didComplete を呼び出すと、返り値として Delegateの didCompleteWithAuthorizationdidCompleteWithError メソッドの値を返します。
(成功か失敗かでどちらかのDelegateメソッドの値を取得するため、成功時にはerrorはnilに、失敗時にはauthorizationはnilになっている感じです。 )

この処理を実現しているのが、サンプルコードの RxASAuthorizationControllerDelegateProxy です。
これらの実装方法はこちらの記事を参考にさせて頂きました。

// 
// RxASAuthorizationControllerDelegateProxy.swift
// 
// DelegateProxyType, DelegateProxyクラスに準拠したクラスを作成
@available(iOS 13.0, *)
public class RxASAuthorizationControllerDelegateProxy: DelegateProxy<ASAuthorizationController, ASAuthorizationControllerDelegate>,
    DelegateProxyType,
    ASAuthorizationControllerDelegate {

    // 成功時と失敗時の値を入れるため、PublishSubjectのジェネリクスの型をタプルで用意
    internal lazy var didComplete = PublishSubject<(ASAuthorization?, Error?)>()

    public func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithAuthorization authorization: ASAuthorization
    ) {
        _forwardToDelegate?.authorizationController(
            controller: controller,
            didCompleteWithAuthorization: authorization
        )
        // タプルで取得しない値の方にnilを設定
        didComplete.onNext((authorization, nil))
    }
    
    public func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithError error: Error
    ) {
        _forwardToDelegate?.authorizationController(
            controller: controller,
            didCompleteWithError: error
        )
        // タプルで取得しない値の方にnilを設定
        didComplete.onNext((nil, error))
    }

    deinit { self.didComplete.on(.completed) }

}

@available(iOS 13.0, *)
extension Reactive where Base: ASAuthorizationController {
    
    // 定義したDelegateProxyの型のdelegateラッパー生成
    public var delegate: DelegateProxy<ASAuthorizationController, ASAuthorizationControllerDelegate> {
        return RxASAuthorizationControllerDelegateProxy.proxy(for: base)
    }
    
    // Delegateメソッドに対応したラッパープロパティの生成
    public var didComplete: Observable<(ASAuthorization?, Error?)> {
        return RxASAuthorizationControllerDelegateProxy
            .proxy(for: base)
            .didComplete
            .asObservable()
    }
    
}
備考
  • ジェネリッククラスの DelegateProxy には下の2つ値をパラメータに設定
    • Rxに対応させたいクラス
    • そのクラスのDelegate

実行結果

ログイン画面で Sing In With Apple ボタンを押下しログインを完了すると、Authentication画面上に登録されたユーザーの情報が表示されます f:id:kamui_project_tony:20191217010209p:plain

まとめ

サンプルコードで簡単なSign in with Appleの実装をまとめてみました。
RxSwiftの対応も意識した作りとなっているため、これから試してみよう, 導入してみようという方のお力になれれば幸いです。

そいえば僕はクリスマスイブが誕生日だったりするのですが、プレゼント代わりにはてなスターとかブックマークとかしてくれると嬉しいです。
ではではメリークリスマスー(・ω・)ノ