How to Sign In with Apple in SwiftUI
Drop SignInWithAppleButton into your view, request
.fullName and .email scopes,
then cast the result to ASAuthorizationAppleIDCredential and save
the opaque user string as your stable user identifier.
import SwiftUI
import AuthenticationServices
struct LoginView: View {
var body: some View {
SignInWithAppleButton(.signIn) { request in
request.requestedScopes = [.fullName, .email]
} onCompletion: { result in
switch result {
case .success(let auth):
guard let cred = auth.credential as? ASAuthorizationAppleIDCredential else { return }
print("User ID:", cred.user)
case .failure(let error):
print("Error:", error.localizedDescription)
}
}
.signInWithAppleButtonStyle(.black)
.frame(height: 50)
.padding(.horizontal, 32)
}
}
Full implementation
The cleanest pattern is an @Observable AuthState class that holds
the current user identifier, name, and email. SwiftUI's conditional navigation then
switches between a LoginView and a WelcomeView based on whether
userID is set. Apple only delivers the full name and email on the
first authorization — save them to your persistent store immediately.
import SwiftUI
import AuthenticationServices
// MARK: - Auth State
@Observable
final class AuthState {
var userID: String?
var fullName: String?
var email: String?
var isSignedIn: Bool { userID != nil }
func handleAuthorization(_ authorization: ASAuthorization) {
guard let credential = authorization.credential
as? ASAuthorizationAppleIDCredential else { return }
// userID is stable across sessions — persist it (e.g. Keychain / SwiftData)
userID = credential.user
// fullName and email arrive ONLY on first sign-in; save them now
let given = credential.fullName?.givenName ?? ""
let family = credential.fullName?.familyName ?? ""
let joined = [given, family].filter { !$0.isEmpty }.joined(separator: " ")
if !joined.isEmpty { fullName = joined }
if let e = credential.email { email = e }
}
func signOut() {
userID = nil
fullName = nil
email = nil
}
}
// MARK: - Root switcher
struct SignInWithAppleView: View {
@State private var authState = AuthState()
var body: some View {
Group {
if authState.isSignedIn {
WelcomeView(authState: authState)
.transition(.opacity)
} else {
LoginView(authState: authState)
.transition(.opacity)
}
}
.animation(.easeInOut, value: authState.isSignedIn)
}
}
// MARK: - Login screen
struct LoginView: View {
let authState: AuthState
@Environment(\.colorScheme) private var colorScheme
var body: some View {
VStack(spacing: 24) {
Spacer()
Image(systemName: "apple.logo")
.font(.system(size: 72, weight: .light))
.foregroundStyle(.primary)
Text("Sign in to continue")
.font(.title2.weight(.semibold))
Spacer()
SignInWithAppleButton(.signIn) { request in
request.requestedScopes = [.fullName, .email]
} onCompletion: { result in
switch result {
case .success(let auth):
authState.handleAuthorization(auth)
case .failure(let error):
// Present an alert in production
print("Sign in failed:", error.localizedDescription)
}
}
.signInWithAppleButtonStyle(colorScheme == .dark ? .white : .black)
.frame(height: 50)
.padding(.horizontal, 40)
.accessibilityLabel("Sign in with Apple")
Text("We never see your Apple ID password.")
.font(.footnote)
.foregroundStyle(.secondary)
.padding(.bottom, 48)
}
}
}
// MARK: - Post-auth welcome screen
struct WelcomeView: View {
let authState: AuthState
var body: some View {
NavigationStack {
VStack(spacing: 16) {
Image(systemName: "checkmark.seal.fill")
.font(.system(size: 64))
.foregroundStyle(.green)
.padding(.top, 60)
Text("Welcome\(authState.fullName.map { ", \($0)" } ?? "")!")
.font(.title.weight(.bold))
.multilineTextAlignment(.center)
if let email = authState.email {
Text(email)
.foregroundStyle(.secondary)
}
Text("User ID: \(authState.userID ?? "—")")
.font(.caption.monospaced())
.foregroundStyle(.tertiary)
.padding(.top, 4)
Spacer()
Button(role: .destructive) {
authState.signOut()
} label: {
Label("Sign Out", systemImage: "rectangle.portrait.and.arrow.right")
}
.buttonStyle(.bordered)
.padding(.bottom, 48)
}
.padding(.horizontal, 32)
.navigationTitle("Dashboard")
.navigationBarTitleDisplayMode(.inline)
}
}
}
#Preview("Signed out") {
SignInWithAppleView()
}
#Preview("Signed in") {
let state = AuthState()
state.userID = "001234.abc.5678"
state.fullName = "Taylor Swift"
state.email = "taylor@example.com"
return WelcomeView(authState: state)
}
How it works
-
@Observable AuthState— The@Observablemacro (introduced in iOS 17) eliminates the need for@PublishedorObservableObject. SwiftUI automatically tracks reads ofuserID,fullName, andemail, re-rendering only the views that consume them. -
SignInWithAppleButtonclosures — The request closure fires before the sheet appears, letting you specify scopes ([.fullName, .email]). The onCompletion closure fires after the user authenticates; theResultis either a success carrying anASAuthorizationor a failure with anError. -
First-sign-in data is ephemeral — In
handleAuthorization(_:),credential.fullNameandcredential.emailare non-nil only the very first time a user authorizes your app. On all subsequent sign-ins they arenil. Persist them to a Keychain item or SwiftData immediately. -
credential.user— This opaque string is your stable, app-scoped user identifier. It survives app reinstalls as long as the user stays signed in with the same Apple ID. Use it as a foreign key in your data store. -
Adaptive button style —
.signInWithAppleButtonStyleis set based oncolorSchemeso the button looks correct in both light (.black) and dark (.white) mode, satisfying Apple's Human Interface Guidelines for the Sign in with Apple button.
Variants
Re-authenticate an existing user on launch
On cold start, check credential state with ASAuthorizationAppleIDProvider before showing the login screen — saves users from having to tap "Sign in" when they're already authorized.
import AuthenticationServices
extension AuthState {
/// Call from .task { } on your root view.
func restoreSessionIfPossible() async {
guard let storedUserID = UserDefaults.standard.string(forKey: "appleUserID") else { return }
let provider = ASAuthorizationAppleIDProvider()
let state = try? await provider.credentialState(forUserID: storedUserID)
if state == .authorized {
userID = storedUserID
// Restore cached name/email from Keychain here
}
// .revoked / .notFound → stay on login screen
}
}
// Usage in root view:
struct SignInWithAppleView: View {
@State private var authState = AuthState()
var body: some View {
Group {
if authState.isSignedIn {
WelcomeView(authState: authState)
} else {
LoginView(authState: authState)
}
}
.task { await authState.restoreSessionIfPossible() }
}
}
Continue / Create Account button label
Pass a different SignInWithAppleButton.Label to the button initializer to change the button copy while keeping Apple's required styling. Use .signIn for returning users, .continue for a generic gate, or .signUp for a registration screen. All three are natively localized by the OS.
// Sign-up flow
SignInWithAppleButton(.signUp) { request in
request.requestedScopes = [.fullName, .email]
} onCompletion: { result in
// handle result
}
.signInWithAppleButtonStyle(.whiteOutline)
.frame(height: 50)
Common pitfalls
- 🔑 Missing entitlement. You must add the Sign in with Apple capability in Xcode under Signing & Capabilities, and enable it in App Store Connect under the App ID. Forgetting either causes a silent failure at runtime — no error, just a cancelled sheet.
-
📧 Email and name arrive once. The biggest SwiftUI-specific gotcha:
credential.fullNameandcredential.emailarenilon every sign-in except the first. If you don't persist them immediately inonCompletion, they're gone. Test this by going to Settings → Apple ID → Password & Security → Sign in with Apple → [Your App] → Stop Using, then re-signing in. -
♿ Accessibility label required. Apple's reviewer guidelines require the Sign in with Apple button to have a clear VoiceOver label. Add
.accessibilityLabel("Sign in with Apple")to the button — the system-provided button doesn't always synthesize a readable label automatically on all iOS versions. -
🌙 Button style in dark mode. Apple's HIG forbids using the
.blackbutton style on dark backgrounds. Always adapt withcolorSchemeor use.whiteOutlinewhen your background is dark — reviewers flag this. - 📱 Simulator vs. device. Sign in with Apple works on the Simulator only if you're signed into an Apple ID in Xcode → Preferences → Accounts. On a physical device the full system sheet appears regardless.
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement Sign in with Apple in SwiftUI for iOS 17+. Use AuthenticationServices: SignInWithAppleButton, ASAuthorizationAppleIDCredential, and ASAuthorizationAppleIDProvider. Manage auth state with @Observable. Persist the user identifier to the Keychain between sessions. Re-check credential state on app launch with credentialState(forUserID:). Make it accessible (VoiceOver labels on the button). Adapt button style for light/dark mode. Add a #Preview with realistic sample data showing both signed-out and signed-in states.
In Soarias's Build phase, paste this prompt into the active session to scaffold the full auth layer — Soarias will wire AuthState into your existing @main app entry point and generate matching SwiftData models in a single pass.
Related
FAQ
Does this work on iOS 16?
SignInWithAppleButton is available from iOS 14 and the .signInWithAppleButtonStyle modifier from iOS 14. The @Observable macro used in this guide requires iOS 17. To support iOS 16, replace @Observable final class AuthState with @MainActor final class AuthState: ObservableObject and annotate each property with @Published.
How do I verify token validity on my server?
The ASAuthorizationAppleIDCredential exposes an identityToken (a short-lived JWT) and an authorizationCode (single-use). Send the authorizationCode to your backend and call Apple's https://appleid.apple.com/auth/token endpoint to exchange it for an access token and refresh token. Use the refresh token for ongoing server-side validation — never pass the raw user string as a trusted auth token.
What's the UIKit equivalent?
In UIKit you instantiate ASAuthorizationAppleIDButton, add it to your view, and trigger an ASAuthorizationController with an ASAuthorizationAppleIDProvider().createRequest(). Implement ASAuthorizationControllerDelegate and ASAuthorizationControllerPresentationContextProviding to handle the result and anchor the sheet. SwiftUI's SignInWithAppleButton wraps all of this in a single declarative view.
Last reviewed: 2026-05-11 by the Soarias team.