[Xcode/Swift] Sign in with Appleを理解してみる② 🍎

前回はこちら:

[Xcode/Swift] Sign in with Appleを理解してみる① 🍎

今回は、実際に手を動かしてSign in with Appleの実装を組み込んで学んでいきましょう。

4. UIKitでの Sign in with Apple 基本実装

流れ:

  • 4.1 ASAuthorizationAppleIDButton を配置する
  • 4.2 認証リクエストを作成する
  • 4.3 ASAuthorizationControllerDelegate で結果を受け取る
  • 4.4 取得できるユーザー情報
  • 4.5 エラー処理とキャンセル

4.1 ASAuthorizationAppleIDButton を配置する

ここでは Storyboard なし / コードでレイアウト のシンプルな例で作成します。

import UIKit
import AuthenticationServices

final class ViewController: UIViewController {

    private let appleIDButton = ASAuthorizationAppleIDButton(
        type: .signIn,
        style: .black
    )

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .systemBackground
        setupAppleIDButton()
    }

    private func setupAppleIDButton() {
        // ボタンタップで認証処理をスタート
        appleIDButton.addTarget(
            self,
            action: #selector(handleAppleIDButtonTap),
            for: .touchUpInside
        )

        // Auto Layout で中央に配置
        appleIDButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(appleIDButton)

        NSLayoutConstraint.activate([
            appleIDButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            appleIDButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            appleIDButton.widthAnchor.constraint(equalToConstant: 248),
            appleIDButton.heightAnchor.constraint(equalToConstant: 48)
        ])
    }

    @objc private func handleAppleIDButtonTap() {
        // ここで認証フローを開始する(次のセクション)
    }
}

ここまでで、

  • 画面中央に「Appleでサインイン」ボタンが出る
  • タップすると handleAppleIDButtonTap() が呼ばれる (適当にprintとかおけばOK)

という状態になります。


4.2 認証リクエストを作成する(ASAuthorizationAppleIDRequest)

ボタンタップ時に、認証リクエストを作ってコントローラを起動

@objc private func handleAppleIDButtonTap() {
        // 1. リクエストを作成
        let provider = ASAuthorizationAppleIDProvider()
        let request = provider.createRequest()

        // 2. どの情報を要求するか(スコープ)を指定
        request.requestedScopes = [.fullName, .email]

        // 3. コントローラを作って、デリゲートを設定
        let authorizationController = ASAuthorizationController(authorizationRequests: [request])
        authorizationController.delegate = self
        authorizationController.presentationContextProvider = self

        // 4. 認証フロー開始(AppleのUIが出てくる)
        authorizationController.performRequests()
    }

ポイント:

  • requestedScopes.fullName, .email を指定すると初回ログイン時にだけ 名前 / メールの共有をお願いできる
  • 2回目以降は基本的にこれらが返ってこないので、「最初の1回でちゃんと保存する」 のが重要

この時点ではdelegate設定をまだしてないのでビルドエラーになりますが一旦OK、次からdelegate設定をします。


4.3 ASAuthorizationControllerDelegate で結果を受け取る

認証結果は ASAuthorizationControllerDelegate 経由で返ってきます。

extension ViewController: ASAuthorizationControllerDelegate {

    // 成功時
    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithAuthorization authorization: ASAuthorization
    ) {
        if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
            handle(credential: appleIDCredential)
        }
    }

    // 失敗・キャンセル時
    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithError error: Error
    ) {
        print("Sign in with Apple failed: \(error.localizedDescription)")
        // 後で 4.5 で詳しく
    }
}

また、認証UIをどこに出すかを伝えるために
ASAuthorizationControllerPresentationContextProviding も実装が必要。

extension ViewController: ASAuthorizationControllerPresentationContextProviding {
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        // 通常は現在表示中の window を返せばOK
        view.window ?? ASPresentationAnchor()
    }
}

4.4 取得できるユーザー情報(userIdentifier, fullName, email)

成功時に渡される ASAuthorizationAppleIDCredential から、
いろいろな情報を取り出せます。

private func handle(credential: ASAuthorizationAppleIDCredential) {
    // ユーザーを一意に表すID(アプリごとに固定)
    let userIdentifier = credential.user

    // 初回ログイン時にのみ入る可能性がある
    let fullName = credential.fullName
    let email = credential.email

    // IDトークン(JWT)
    let identityTokenData = credential.identityToken
    let authorizationCodeData = credential.authorizationCode

    print("userIdentifier: \(userIdentifier)")
    print("fullName: \(String(describing: fullName))")
    print("email: \(String(describing: email))")
    print("identityToken: \(String(data: identityTokenData ?? Data(), encoding: .utf8) ?? "")")

    // ▼ ここで自分のアプリのユーザーとして扱う処理を書くイメージ
    // 例:
    // 1. userIdentifier を自前の User テーブルに保存
    // 2. 初回なら fullName / email も一緒に保存
    // 3. 必要なら identityToken をサーバーに送って検証
}

ポイント:

  • credential.user
    • これが 「このアプリの中で、その人を一意に表すID」 になる
    • 自前DBの appleUserID として保存するのがGood
  • credential.fullName / credential.email
    • 通常、最初の1回しか返ってこない
    • nil になることも多いので、必ず Optional として扱う
  • identityToken / authorizationCode
    • 本格的にやるならサーバーに送って検証
    • 個人開発で「まずはログインだけ体験したい」段階では、
      ひとまず user だけ使ってもOK

4.5 エラー処理とキャンセル時のハンドリング

didCompleteWithError では、
ユーザーキャンセルも含めた「失敗ケース」が飛んでくる。

func authorizationController(
    controller: ASAuthorizationController,
    didCompleteWithError error: Error
) {
    if let authorizationError = error as? ASAuthorizationError {
        switch authorizationError.code {
        case .canceled:
            print("ユーザーがキャンセルしました")
            // ここでは何もせず、画面をそのままにしておくなど
        case .failed:
            print("認証に失敗しました")
        case .invalidResponse:
            print("無効なレスポンスです")
        case .notHandled:
            print("処理されませんでした")
        case .unknown:
            print("不明なエラーです")
        @unknown default:
            print("将来のOSバージョンで追加されたエラーです")
        }
    } else {
        print("Sign in with Apple Error: \(error.localizedDescription)")
    }
}

ここでやっておきたいこと:

  • キャンセルと、本当のエラーを区別する
    • キャンセル → ユーザー操作なので、静かに何もしない
    • その他 → アラート表示などで「もう一度お試しください」と案内
  • ログをきちんと残す
    • 開発中は print でOK
    • 実務ではログ基盤(Firebase Crashlytics 等)と連携すると◎

5. SwiftUIでの Sign in with Apple 基本実装

UIKit 版では ASAuthorizationControllerDelegate を直接触りましたが、
SwiftUI ではもう少しラクに書けます。

流れ:

  • 5.1 SignInWithAppleButton の基本的な使い方
  • 5.2 Coordinatorパターンで UIKit API をラップする場合
  • 5.3 認証結果からユーザー情報を取り出す
  • 5.4 ViewModel と組み合わせるサンプル

こちらで進めていきます。


5.1 SignInWithAppleButton の使い方

SwiftUI には、AuthenticationServices フレームワークが用意している
ネイティブの SwiftUI 用ボタンがあります。

import SwiftUI
import AuthenticationServices

struct ContentView: View {
    var body: some View {
        SignInWithAppleButton(.signIn) { request in
            // どの情報を要求するか
            request.requestedScopes = [.fullName, .email]
        } onCompletion: { result in
            // 認証結果はここで受け取る(後で詳しく)
            switch result {
            case .success(let authorization):
                print("success: \(authorization)")
            case .failure(let error):
                print("error: \(error.localizedDescription)")
            }
        }
        .signInWithAppleButtonStyle(.black)
        .frame(height: 44)
        .padding()
    }
}

ポイント:

  • onRequest
    どんな情報(スコープ)を要求するかを指定する場所
  • onCompletion
    → 認証成功/失敗の結果が Result<ASAuthorization, Error> で返ってくる

UI とロジックを 同じ SwiftUI View の中で完結できるのがメリット。


5.2 Coordinatorパターンで UIKit API をラップする

上の SignInWithAppleButton だけで済むならそれで十分ですが、

  • 既に UIKit で組んだ認証処理を SwiftUI から再利用したい
  • ASAuthorizationControllerDelegate をそのまま使いたい
  • より細かく認証フローを制御したい

といった場合は、UIKit の ASAuthorizationAppleIDButton を SwiftUI からラップする手もアリ。

struct AppleSignInButton: UIViewRepresentable {

    // 結果を呼び出し元に返すためのクロージャ
    var onCompletion: (Result<ASAuthorizationAppleIDCredential, Error>) -> Void

    func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
        let button = ASAuthorizationAppleIDButton(type: .signIn, style: .black)
        button.addTarget(context.coordinator,
                         action: #selector(Coordinator.handleTap),
                         for: .touchUpInside)
        return button
    }

    func updateUIView(_ uiView: ASAuthorizationAppleIDButton, context: Context) {
        // 見た目を更新したければここに書く
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    // MARK: - Coordinator

    class Coordinator: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
        let parent: AppleSignInButton

        init(_ parent: AppleSignInButton) {
            self.parent = parent
        }

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

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

        // 成功
        func authorizationController(
            controller: ASAuthorizationController,
            didCompleteWithAuthorization authorization: ASAuthorization
        ) {
            if let credential = authorization.credential as? ASAuthorizationAppleIDCredential {
                parent.onCompletion(.success(credential))
            }
        }

        // 失敗 / キャンセル
        func authorizationController(
            controller: ASAuthorizationController,
            didCompleteWithError error: Error
        ) {
            parent.onCompletion(.failure(error))
        }

        // 認証UIをどのウィンドウに出すか
        func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
            // サンプルなので「一番手前の window を返す」くらいの雑実装
            UIApplication.shared.connectedScenes
                .compactMap { $0 as? UIWindowScene }
                .flatMap { $0.windows }
                .first { $0.isKeyWindow } ?? ASPresentationAnchor()
        }
    }
}

// このラッパーを SwiftUI から使うと
struct ContentView: View {
    var body: some View {
        AppleSignInButton { result in
            switch result {
            case .success(let credential):
                print("user: \(credential.user)")
            case .failure(let error):
                print("error: \(error.localizedDescription)")
            }
        }
        .frame(height: 44)
        .padding()
    }
}

5.3 認証結果からユーザー情報を取り出す

SignInWithAppleButtononCompletion、または
ラッパーの onCompletion で受け取った情報から、
実際に ユーザーID / 名前 / メール を取り出してみましょう。

SignInWithAppleButton の場合:

SignInWithAppleButton(.signIn) { request in
    request.requestedScopes = [.fullName, .email]
} onCompletion: { result in
    switch result {
    case .success(let authorization):
        if let credential = authorization.credential as? ASAuthorizationAppleIDCredential {
            handle(credential: credential)
        }
    case .failure(let error):
        print("Sign in with Apple error: \(error.localizedDescription)")
    }
}

func handle(credential: ASAuthorizationAppleIDCredential) {
    let userIdentifier = credential.user
    let fullName = credential.fullName
    let email = credential.email
    let identityTokenData = credential.identityToken

    let identityToken = identityTokenData.flatMap { String(data: $0, encoding: .utf8) }

    print("userIdentifier: \(userIdentifier)")
    print("fullName: \(String(describing: fullName))")
    print("email: \(String(describing: email))")
    print("identityToken: \(identityToken ?? "nil")")

    // ▼ ここで ViewModel などに渡して状態更新する想定
}

ここでも UIKit 版と同じく、

  • user → 自前ユーザーIDとして DB に保存する超重要な値
  • fullName / email基本「最初の1回だけ」 返ってくる
  • identityToken → サーバー側で検証したいときに使う

という役割は同じ。


5.4 ViewModel と組み合わせたシンプルなアーキテクチャ例

認証結果を ViewModel (ObservableObject) に渡して状態管理するのもまたヨシ。

struct AppleUser {
    let id: String
    let name: String?
    let email: String?
}

final class AuthViewModel: ObservableObject {
    @Published var currentUser: AppleUser?
    @Published var errorMessage: String?

    func handleAuthorization(result: Result<ASAuthorization, Error>) {
        switch result {
        case .success(let authorization):
            guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else {
                errorMessage = "認証情報の取得に失敗しました"
                return
            }
            handle(credential: credential)

        case .failure(let error):
            handle(error: error)
        }
    }

    private func handle(_ credential: ASAuthorizationAppleIDCredential) {
        let id = credential.user

        // 最初の1回だけ入る可能性がある
        let name = [credential.fullName?.givenName, credential.fullName?.familyName]
            .compactMap { $0 }
            .joined(separator: " ")
        let email = credential.email

        let user = AppleUser(id: id,
                             name: name.isEmpty ? nil : name,
                             email: email)

        // ここでDB保存などを行うのが実務的
        currentUser = user
        errorMessage = nil
    }

    private func handle(error: Error) {
        if let authError = error as? ASAuthorizationError,
           authError.code == .canceled {
            // キャンセルはエラーではなく「ユーザーの選択」として扱うことも多い
            print("ユーザーがキャンセルしました")
            return
        }
        errorMessage = error.localizedDescription
    }
}

View 側での利用イメージ:

struct ContentView: View {
    @StateObject private var viewModel = AuthViewModel()

    var body: some View {
        VStack(spacing: 16) {
            if let user = viewModel.currentUser {
                VStack {
                    Text("ログイン中: \(user.id)")
                        .font(.footnote)
                    if let name = user.name {
                        Text("名前: \(name)")
                    }
                    if let email = user.email {
                        Text("メール: \(email)")
                            .font(.caption)
                            .foregroundColor(.secondary)
                    }
                }
            } else {
                Text("ログインしていません")
                    .foregroundColor(.secondary)
            }

            SignInWithAppleButton(.signIn) { request in
                request.requestedScopes = [.fullName, .email]
            } onCompletion: { result in
                viewModel.handleAuthorization(result: result)
            }
            .signInWithAppleButtonStyle(.black)
            .frame(height: 44)
            .padding(.horizontal)
        }
        .padding()
        .alert("エラー", isPresented: Binding(
            get: { viewModel.errorMessage != nil },
            set: { _ in viewModel.errorMessage = nil }
        )) {
            Button("OK", role: .cancel) { }
        } message: {
            Text(viewModel.errorMessage ?? "")
        }
    }
}

UIKit, SwiftUIのまとめ

UIKit:

  1. ASAuthorizationAppleIDButton を画面に置く
  2. タップ時に ASAuthorizationAppleIDProvider からリクエストを作る
  3. ASAuthorizationController を使って認証UIを出す
  4. デリゲートで成功 → ASAuthorizationAppleIDCredential からユーザー情報取得、失敗 → ASAuthorizationError を分岐して処理

SwiftUI:

  1. SignInWithAppleButton を画面に配置する
  2. onRequest で欲しい情報(スコープ)を指定する
  3. onCompletion で成功 or 失敗を受け取る
  4. 認証成功時は ASAuthorizationAppleIDCredential から情報を取り出す

これで実装の基本は完了です、最後にハマりポイント & Tipsを見て理解を深めてSign in with Appleの使い手になりましょう。


ハマりポイント & Tips 集

6.1 名前・メールアドレスは「初回の一度きり」しか取れない

Sign in with Apple の罠。

基本仕様

  • fullNameemail初回ログイン時だけ返ってくる可能性がある
  • 同じユーザーが2回目以降ログイン → ほとんどの場合 nil

対策

  • 取得できた瞬間に確実に保存する(Firebase連携なら勝手にこの辺りうまくやってくれる記憶)
  • 「あとで取得し直す」は不可能だと思うこと
  • 初回ログイン時と2回目以降で UI を変えたい場合は、email が nilかどうかで判断すると自然

6.2 非公開メールアドレス(Relay)の扱い

ユーザーが「メールを隠す」を選ぶと、
Apple が発行した ランダムなリレー用メール が降ってくる。

xxxxxx@privaterelay.appleid.com

注意点

  • ユーザー本人の Apple ID 設定で「このアプリの接続解除」をされるとメール転送が止まる
  • 実務でメール配信が重要なら、メールが届かない可能性があります」程度のガイダンスは入れた方が良い

Tips

  • リレーアドレスも普通のメールアドレスと同じように送信してOK (Apple側が裏で転送してくれる)
  • 本物のメールアドレスを後から取得」する手段はない
  • 最初の1回で本物を渡したかどうかは、ユーザー任せ

6.3 シミュレータ・実機で動かないときに見直すチェックリスト

Sign in with Apple が正しく動かず、
認証画面が一瞬で消える or 無反応 のときは、大体これらが原因。

チェックリスト

  • Apple ID にログインした実機でテストしているか
  • Xcode の Team が正しいアカウントになっているか
  • Bundle ID が Developer サイトの設定と一致しているか
  • プロジェクトに Sign in with Apple Capability を追加したか
  • iOS が 13以上
  • presentationContextProvider を実装しているか(UIKit)

それでも動かない時

一度 Xcode の DerivedData を消すか、
Sign in with Apple Capability を付け直すと直ることがある。


6.4 user(ユーザー識別子)は「アプリ専用」な点に注意

Apple が返す credential.user
アプリ(Bundle ID)ごとに固有

なので、

同じ Apple ID でログインしてアプリA と アプリB では 異なる user が返る

この点に注意。


6.5 “ログアウト” と “再認証” の概念がやや特殊

Sign in with Apple には概念上:

本当の意味でのログアウトは存在しない

iOS が「Apple ID でのログイン状態」を管理しているため、
ユーザーがログアウトしても、再ログイン時は Face ID 一発で通ることがほとんど。

Tips

  • 自アプリ側では「アプリ内のログイン状態」をリセットするだけでOK
  • ユーザーは iOS 設定で Apple 連携を解除できる
  • ↑の場合、次回ログイン時に 初回扱いになり、メール/名前が再取得される可能性あり

6.6 UI/UX の小技

Tips

  • Appleでサインイン」ボタンは目立つ位置に置く
    • ホーム画面 or 会員登録画面の一番上が最適
  • ボタンは 44pt 以上の高さを確保
  • ロード中は ProgressView を必ず出す(複数タップを防ぐ、ただこれはApple Sign Inが勝手にうまくやってくれた記憶、少し曖昧)
  • エラー時は
    • キャンセル → 静かに無視
    • 本当のエラー → きちんとメッセージを表示
      のように扱いを分けるとユーザーが混乱しにくい

総括

Sign in with Appleの解説はこれで以上になります。

この記事で学んだことを活かして、ぜひあなたのプロジェクトにもSign in with Appleを導入してみてください。

ここで解説したことは表面上の概念や基本的な実装とTipsくらいなので、もっと深く知りたい場合は公式ドキュメントを読んだり、生成AIなどをうまく活用して理解の幅を広げるとより良いかと思います。

今回は以上です、お読みいただきありがとうございました。