前回はこちら:
今回は、実際に手を動かしてSign in with Appleの実装を組み込んで学んでいきましょう。
流れ:
- 4.1 ASAuthorizationAppleIDButton を配置する
- 4.2 認証リクエストを作成する
- 4.3 ASAuthorizationControllerDelegate で結果を受け取る
- 4.4 取得できるユーザー情報
- 4.5 エラー処理とキャンセル
ここでは 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)
という状態になります。
ボタンタップ時に、認証リクエストを作ってコントローラを起動。
@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設定をします。
認証結果は 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()
}
}成功時に渡される 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 等)と連携すると◎
- 開発中は
UIKit 版では ASAuthorizationControllerDelegate を直接触りましたが、
SwiftUI ではもう少しラクに書けます。
流れ:
- 5.1
SignInWithAppleButtonの基本的な使い方 - 5.2 Coordinatorパターンで UIKit API をラップする場合
- 5.3 認証結果からユーザー情報を取り出す
- 5.4 ViewModel と組み合わせるサンプル
こちらで進めていきます。
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 の中で完結できるのがメリット。
上の 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()
}
}SignInWithAppleButton の onCompletion、または
ラッパーの 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→ サーバー側で検証したいときに使う
という役割は同じ。
認証結果を 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:
ASAuthorizationAppleIDButtonを画面に置く- タップ時に
ASAuthorizationAppleIDProviderからリクエストを作る ASAuthorizationControllerを使って認証UIを出す- デリゲートで成功 →
ASAuthorizationAppleIDCredentialからユーザー情報取得、失敗 →ASAuthorizationErrorを分岐して処理
SwiftUI:
SignInWithAppleButtonを画面に配置するonRequestで欲しい情報(スコープ)を指定するonCompletionで成功 or 失敗を受け取る- 認証成功時は
ASAuthorizationAppleIDCredentialから情報を取り出す
これで実装の基本は完了です、最後にハマりポイント & Tipsを見て理解を深めてSign in with Appleの使い手になりましょう。
Sign in with Apple の罠。
基本仕様
fullNameとemailは 初回ログイン時だけ返ってくる可能性がある- 同じユーザーが2回目以降ログイン → ほとんどの場合 nil
対策
- 取得できた瞬間に確実に保存する(Firebase連携なら勝手にこの辺りうまくやってくれる記憶)
- 「あとで取得し直す」は不可能だと思うこと
- 初回ログイン時と2回目以降で UI を変えたい場合は、email が nilかどうかで判断すると自然
ユーザーが「メールを隠す」を選ぶと、
Apple が発行した ランダムなリレー用メール が降ってくる。
xxxxxx@privaterelay.appleid.com注意点
- ユーザー本人の Apple ID 設定で「このアプリの接続解除」をされるとメール転送が止まる
- 実務でメール配信が重要なら、メールが届かない可能性があります」程度のガイダンスは入れた方が良い
Tips
- リレーアドレスも普通のメールアドレスと同じように送信してOK (Apple側が裏で転送してくれる)
- 本物のメールアドレスを後から取得」する手段はない
- 最初の1回で本物を渡したかどうかは、ユーザー任せ
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 を付け直すと直ることがある。
Apple が返す credential.user は
アプリ(Bundle ID)ごとに固有。
なので、
同じ Apple ID でログインしてアプリA と アプリB では 異なる user が返る
この点に注意。
Sign in with Apple には概念上:
本当の意味でのログアウトは存在しない
iOS が「Apple ID でのログイン状態」を管理しているため、
ユーザーがログアウトしても、再ログイン時は Face ID 一発で通ることがほとんど。
Tips
- 自アプリ側では「アプリ内のログイン状態」をリセットするだけでOK
- ユーザーは iOS 設定で Apple 連携を解除できる
- ↑の場合、次回ログイン時に 初回扱いになり、メール/名前が再取得される可能性あり
Tips
- 「Appleでサインイン」ボタンは目立つ位置に置く
- ホーム画面 or 会員登録画面の一番上が最適
- ボタンは 44pt 以上の高さを確保
- ロード中は ProgressView を必ず出す(複数タップを防ぐ、ただこれはApple Sign Inが勝手にうまくやってくれた記憶、少し曖昧)
- エラー時は
- キャンセル → 静かに無視
- 本当のエラー → きちんとメッセージを表示
のように扱いを分けるとユーザーが混乱しにくい
Sign in with Appleの解説はこれで以上になります。
この記事で学んだことを活かして、ぜひあなたのプロジェクトにもSign in with Appleを導入してみてください。
ここで解説したことは表面上の概念や基本的な実装とTipsくらいなので、もっと深く知りたい場合は公式ドキュメントを読んだり、生成AIなどをうまく活用して理解の幅を広げるとより良いかと思います。
今回は以上です、お読みいただきありがとうございました。
