It’s now or never

IT系の技術ブログです。気になったこと、勉強したことを備忘録的にまとめて行きます。

【SwifUI】Sign In With Appleの機能をSwiftUIで実装する

概要

アップル独自の認証方法「Sign in with Apple」について、SwiftUIを使って実装してみます。

環境

  • XCode: Version 11.3.1
  • Swift: Version 5.1

Sign in with appleのボタンの作成

まず、はじめにボタンコンポーネントの作成から実装します。

Sign in with appleに使用するボタンについては、Apple側で ASAuthorizationAppleIDButton という標準のボタンコンポーネントを用意してくれています。
これを使うとわざわざデザインしなくても公式のボタンデザインが使用できるので通常はこのコンポーネントを使うのがいいのではないかと思います。
(認証機能自体は別実装ですので独自デザインも可能です)

ただし、ASAuthorizationAppleIDButtonは、SwiftUIの標準コンポーネントとしては提供されていません。
そのため 、まずはこのボタンをSwiftUIのViewコンポーネントとして使えるようにします。

import SwiftUI
import AuthenticationServices

struct SignInWithAppleButton: UIViewRepresentable {
    func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
        let button = ASAuthorizationAppleIDButton()
        return button
    }
    
    func updateUIView(_: ASAuthorizationAppleIDButton, context _: Context) {}
}

UIKitで用意されているUIコンポーネントをSwiftUIのViewコンポーネントに橋渡しするためには、UIViewRepresentable を適用したstructを用意します。

今回は、UIViewRepresentableについての詳細は割愛しますが、このプロトコルmakeUIView というメソッドを実装し、表示したいUIコンポーネントを返すと、SwiftUI側でViewコンポーネントとして使用できるようになります。

またASAuthorizationAppleIDButton を使用するためには、 AuthenticationServices というパッケージが必要のためimportしています。

ボタンの準備はこれだけでOKです。あとはこのボタンをSwiftUIの画面に配置すればSign in With Appleのボタンを表示することができます。

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            SignInWithAppleButton()
                .frame(width: 200.0, height: 60.0)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

f:id:inon29:20200307170022p:plain

ボタンには、サイズ指定をしていないため frame Modifierを使ってサイズを指定しています。

ASAuthorizationAppleIDButton には、ボタンの種別(authorizationButtonType)とボタンのスタイル(authorizationButtonStyle)の2つのプロパティを設定することができます。

authorizationButtonTypeには、

  • continue
  • signUp
  • signIn

authorizationButtonStyleには、

  • black
  • whiteOutline
  • white

が指定でき、用途やアプリのデザイン別にボタンを出し分けることも可能です。

f:id:inon29:20200307170052p:plain

ASAuthorizationAppleIDButton(authorizationButtonType: .signUp, 
                            authorizationButtonStyle: .whiteOutline)

authorizationButtonTypeを signUp、authorizationButtonStyleを whiteOutlineに設定するとこのようになります。

認証処理の実装

ボタンの見た目を作ることができたので、ここからは認証処理を実装していきます。

Sign In with Appleの認証処理を行うにはASAuthorizationAppleIDProviderASAuthorizationControllerを使用します。

@objc func didTapButton() {
    let appleIDProvider = ASAuthorizationAppleIDProvider()
    let request = appleIDProvider.createRequest()
    request.requestedScopes = [.fullName, .email]

    let authorizationController = ASAuthorizationController(authorizationRequests: [request])
    authorizationController.delegate = self
    authorizationController.presentationContextProvider = self
    authorizationController.performRequests()
}

上記がリクエストの処理のサンプルです。
ASAuthorizationAppleIDProvider を使って認証リクエストを作成して ASAuthorizationController で認証を実行するという流れになります。

この処理を先程作成した「SignInWithAppleButton」に実装します。

プロジェクトの設定

実装に入る前にプロジェクトの設定をしておきましょう。

Sign In with Appleを利用するには「Target -> Signing&Capabilities -> + Capability」から Sign In with Apple を選択して追加しておく必要があります。

※ この設定を追加しておかないと、実行時にエラーが発生します。

f:id:inon29:20200307160354p:plain f:id:inon29:20200307160406p:plain

実装

次に、「SignInWithAppleButton」に認証処理を追加します。

UIViewRepresentableを適合したコンポーネントは、Coordinator というクラスを使ってコンポーネント上の処理を実装することができます。

今回は、このCoordinatorクラスを使って認証処理を実装します。

import SwiftUI
import AuthenticationServices

struct SignInWithAppleButton: UIViewRepresentable {
    func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
        let button = ASAuthorizationAppleIDButton()
        // NOTE: ボタン押下時のイベント処理を追加
        // CoordinatorクラスのdidTapButtonメソッドでイベントを受け取る
        button.addTarget(context.coordinator,
                         action: #selector(Coordinator.didTapButton),
                         for: .touchUpInside)
        return button
    }
    
    func updateUIView(_: ASAuthorizationAppleIDButton, context _: Context) {}
   
    func makeCoordinator() -> Coordinator {
        // NOTE: Coordinatorを作成する処理
        // 初期値にView自身を渡す
        Coordinator(self)
    }
}

final class Coordinator: NSObject {
    var parent: SignInWithAppleButton

    init(_ parent: SignInWithAppleButton) {
        self.parent = parent
        super.init()
    }
   
    // ボタンコンポーネントをタップされたときの処理
    @objc func didTapButton() {
        // NOTE: リクエストの作成
        let appleIDProvider = ASAuthorizationAppleIDProvider()
        let request = appleIDProvider.createRequest()
        // NOTE: 認証情報として、「ユーザ名」と「メールアドレス」を受け取る
        request.requestedScopes = [.fullName, .email]

        // NOTE: 認証リクエストの実行
        let authorizationController = ASAuthorizationController(authorizationRequests: [request])
        authorizationController.delegate = self
        authorizationController.presentationContextProvider = self
        authorizationController.performRequests()
    }
}

extension Coordinator: ASAuthorizationControllerDelegate {
    // 認証処理が完了した時のコールバック
    func authorizationController(controller _: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
            print("認証成功", appleIDCredential)
        }
    }
    
    // 認証処理が失敗したときのコールバック
    // NOTE: 認証画面でキャンセルボタンを押下されたときにも呼ばれる
    func authorizationController(controller _: ASAuthorizationController, didCompleteWithError error: Error) {
            print("認証エラー", error)
    }
}

extension Coordinator: ASAuthorizationControllerPresentationContextProviding {
    // 認証プロセス(認証ダイアログ)を表示するためのUIWindowを返すためのコールバック
    func presentationAnchor(for _: ASAuthorizationController) -> ASPresentationAnchor {
        let vc = UIApplication.shared.windows.last?.rootViewController
        return (vc?.view.window!)!
    }
}

ボタンを押下されたときのイベントを Coodinator クラスで受け取って(didTapButton)認証リクエストを実行します。

今回は、認証情報として「ユーザ名(.fullName)」と「メールアドレス(.email)」を取得するようにScopeを設定しています。

Coordinator クラスでは、認証処理のdelegate(ASAuthorizationControllerDelegate)と認証プロセスを提供するためのUIWindowを指定するコールバック(ASAuthorizationControllerPresentationContextProviding)を実装する必要があります。

これで実装は完了です。

動作確認

f:id:inon29:20200307163854p:plain

アプリを起動して、ボタンを押下すると上記のようなダイアログが表示され認証処理が開始されます。
正しく認証が行われると結果をdelegateで受け取れます。

あとは、コールバック処理を「SignInWithAppleButton」に追加するなど対応すれば、SwiftUI上で認証結果を受け取れるようになります。

注意事項

個人的に気になったことがあったので記載しておきます。

① 認証ダイアログで「キャンセル」ボタンを押下されたときの挙動

認証ダイアログで「キャンセル」ボタンを押下されると、現状の挙動だと ASAuthorizationControllerDelegate のコールバックである authorizationController(controller _: ASAuthorizationController, didCompleteWithError error: Error) が呼ばれるようです。

個人的にはエラーとキャンセルで処理を分けたかったのですが、今はおそらく同じ箇所に入るためエラーコードで判断するなどの必要があるかもしれません。

② 認証の付加情報は2回目以降は、返ってこない(nilになる)

authorizationController(controller _: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) では認証成功の結果として credential情報が受け取れるのですが、requestScopesで指定した付加情報は2回目以降の認証では入っていません(nilになっています)。
(ID情報であるUserIdは常に返却されます)

そのため、この情報をサーバなどで処理する場合は、初回の認証時にどこかアプリ側で保存しておく必要があります。

この認証履歴は、アプリ内で保有されるものではないためアプリをアンインストールしてもリセットされないため注意が必要です。

(ユーザ自身が「設定 -> パスワードとセキュリティ -> Apple IDを使用中のApp」から対象のアプリの認証情報を削除するとリセットされます)

初回で取れた情報は、キーチェーンに保存するなどしてリカバリ方法を検討する必要があるかもしれません。

ソースコード

今回の実装のソースコードは、ここにおいてあります。

所感

実装は、思ったよりも大分シンブルでした。
他サービス認証(Facebook, Google)などと比較して、SDKを入れる必要もないことから実装コストは大分小さい印象です。
またユーザ観点からも、Apple認証を使うほうがセキュリティ的な安心感もあるため、今後は少なくともiOSアプリについてはSign In with Appleが主流になってくるのかなと思っています。