```html SwiftUI: How to Sign In with Apple (iOS 17+, 2026)

How to Sign In with Apple in SwiftUI

iOS 17+ Xcode 16+ Intermediate APIs: AuthenticationServices Updated: May 11, 2026
TL;DR

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

  1. @Observable AuthState — The @Observable macro (introduced in iOS 17) eliminates the need for @Published or ObservableObject. SwiftUI automatically tracks reads of userID, fullName, and email, re-rendering only the views that consume them.
  2. SignInWithAppleButton closures — The request closure fires before the sheet appears, letting you specify scopes ([.fullName, .email]). The onCompletion closure fires after the user authenticates; the Result is either a success carrying an ASAuthorization or a failure with an Error.
  3. First-sign-in data is ephemeral — In handleAuthorization(_:), credential.fullName and credential.email are non-nil only the very first time a user authorizes your app. On all subsequent sign-ins they are nil. Persist them to a Keychain item or SwiftData immediately.
  4. 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.
  5. Adaptive button style.signInWithAppleButtonStyle is set based on colorScheme so 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

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.

```