The Swift Kit logoThe Swift Kit
Tutorial

Sign in with Apple SwiftUI Tutorial — Complete 2026 Implementation Guide

Everything you need to implement Sign in with Apple in a SwiftUI app. From Xcode capabilities to Supabase token exchange, nonce generation with CryptoKit, handling first-sign-in-only data, credential revocation, and the dozen gotchas Apple does not warn you about.

Ahmed GaganAhmed Gagan
12 min read

Why Sign in with Apple Matters in 2026

Let me start with the rule that catches every new developer off guard: if your app offers any third-party social login (Google, Facebook, X, etc.), you are required by Apple to also offer Sign in with Apple. This is not a suggestion. It is App Store Review Guideline 4.8, and Apple enforces it aggressively. I have personally had two app submissions rejected for missing SIWA when I already had Google sign-in enabled.

But beyond the requirement, Sign in with Apple is genuinely excellent for users:

  • Privacy-first: Users can hide their real email address behind Apple's private relay service. Apple generates a unique, random email like dpdg4x7m3z@privaterelay.appleid.com that forwards to the user's real inbox.
  • No passwords: Authentication is handled via Face ID, Touch ID, or device passcode. Zero friction.
  • Trust: Users trust Apple more than most third-party auth providers. In a 2025 SurveyMonkey study, 67% of iPhone users said they prefer Sign in with Apple over Google or Facebook login.
  • Built-in spam protection: The private relay email means users never have to worry about your app selling their email address.

The catch? Apple's implementation has some genuinely surprising behaviors that trip up even experienced developers. I will cover every single one of them in this tutorial.

The Complete SIWA Flow Explained

Before writing any code, let me walk you through the entire authentication flow so you understand what happens at each step. This will save you hours of debugging later.

  1. User taps the Sign in with Apple button in your SwiftUI view.
  2. iOS presents the Apple authentication sheet. The user sees your app name, chooses whether to share or hide their email, and authenticates with Face ID / Touch ID / passcode.
  3. Apple returns an ASAuthorization result containing an identity token (JWT), an authorization code, and optionally the user's name and email.
  4. Your app generates a cryptographic nonce before the request and includes its SHA-256 hash in the authorization request. The nonce comes back inside the identity token, preventing replay attacks.
  5. Your app sends the identity token and nonce to your backend (Supabase, Firebase, your own server).
  6. Your backend validates the token against Apple's public keys, verifies the nonce, and creates or updates the user session.
  7. The user is authenticated. Your app receives a session token from the backend.

The important thing to understand is that your app never sees the user's Apple ID password. Apple handles all the credential verification on-device and gives you a signed JWT that your backend can independently verify.

Step 1: Xcode Setup — Capabilities and Entitlements

First, you need to enable the Sign in with Apple capability in Xcode:

  1. Open your project in Xcode and select your app target.
  2. Go to the Signing & Capabilities tab.
  3. Click + Capability and search for "Sign in with Apple."
  4. Add it. Xcode will automatically update your entitlements file.

After adding the capability, your .entitlements file should contain:

<key>com.apple.developer.applesignin</key>
<array>
    <string>Default</string>
</array>

If you are using automatic signing (which you should be for indie development), Xcode handles the provisioning profile update automatically. If you are using manual signing, you will need to regenerate your provisioning profile from the Apple Developer Portal after enabling the App ID capability.

Step 2: Apple Developer Portal Configuration

Navigate to Certificates, Identifiers & Profiles in the Apple Developer Portal. You need to configure three things:

2a. App ID

Your App ID should already exist. Go to Identifiers → App IDs, find your app, and confirm that "Sign in with Apple" is checked under Capabilities. If it is not, enable it and save.

2b. Service ID (for web or backend validation)

If your backend (like Supabase) needs to validate tokens independently, you need a Service ID:

  1. Go to Identifiers → Service IDs and click the + button.
  2. Enter a description (e.g., "My App - Web Auth") and an identifier (e.g., com.yourcompany.yourapp.web).
  3. Enable "Sign in with Apple" and click Configure.
  4. Set the Primary App ID to your main app.
  5. Add your backend's callback URL to the Return URLs (for Supabase, this is typically https://YOUR_PROJECT.supabase.co/auth/v1/callback).

2c. Key for Server-to-Server Communication

Some backend providers need a private key to communicate with Apple's servers:

  1. Go to Keys and click +.
  2. Name the key (e.g., "SIWA Key"), enable "Sign in with Apple," and configure it for your Primary App ID.
  3. Download the .p8 key file. Store this securely. Apple only lets you download it once.
  4. Note the Key ID — you will need it for your backend configuration.

Step 3: Backend Setup with Supabase

I am using Supabase as the backend because it has first-class support for Sign in with Apple and a generous free tier. If you are new to Supabase with SwiftUI, check out our complete Supabase SwiftUI tutorial first.

In your Supabase dashboard:

  1. Go to Authentication → Providers.
  2. Find Apple and enable it.
  3. Enter your Service ID (the identifier from step 2b, e.g., com.yourcompany.yourapp.web).
  4. Enter your Secret Key — this is the contents of the .p8 file you downloaded.
  5. Enter your Team ID (found in the top-right of the Apple Developer Portal, a 10-character string).
  6. Enter your Key ID (from step 2c).
  7. Save the configuration.

Supabase now knows how to validate Apple identity tokens and will automatically create users in yourauth.users table when they sign in.

Step 4: SwiftUI Implementation

Now for the fun part. Let us build the actual Sign in with Apple flow in SwiftUI. This involves four pieces: nonce generation, the button, the authorization handler, and the Supabase token exchange.

4a. Nonce Generation with CryptoKit

A nonce is a random string that prevents replay attacks. You generate a random nonce, hash it with SHA-256, and send the hash to Apple. Apple includes the hash in the identity token, so your backend can verify the token was generated for this specific sign-in attempt. Here is the implementation:

// Utilities/NonceGenerator.swift
import CryptoKit
import Foundation

enum NonceGenerator {
    /// Generates a random nonce string of the specified length.
    static func randomNonce(length: Int = 32) -> String {
        precondition(length > 0)
        var randomBytes = [UInt8](repeating: 0, count: length)
        let errorCode = SecRandomCopyBytes(kSecRandomDefault, randomBytes.count, &randomBytes)
        guard errorCode == errSecSuccess else {
            fatalError("Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)")
        }
        let charset: [Character] = Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
        return String(randomBytes.map { charset[Int($0) % charset.count] })
    }

    /// Returns the SHA-256 hash of the input string, encoded as a hex string.
    static func sha256(_ input: String) -> String {
        let inputData = Data(input.utf8)
        let hashed = SHA256.hash(data: inputData)
        return hashed.compactMap { String(format: "%02x", $0) }.joined()
    }
}

Note that we store the raw nonce (before hashing) and send the hashed nonce to Apple. Later, we send the raw nonce to Supabase so it can independently hash it and compare against what is inside the identity token. This is a critical detail — if you mix up which nonce goes where, authentication will silently fail.

4b. The Sign in with Apple Button

Apple provides a native SignInWithAppleButton in the AuthenticationServicesframework. Here is the complete view:

// Views/Auth/AppleSignInView.swift
import AuthenticationServices
import SwiftUI

struct AppleSignInView: View {
    @Environment(\.colorScheme) private var colorScheme
    @State private var currentNonce: String?
    @State private var isLoading = false
    @State private var errorMessage: String?

    let onSuccess: () -> Void

    var body: some View {
        VStack(spacing: 16) {
            SignInWithAppleButton(
                .signIn,
                onRequest: configureRequest,
                onCompletion: handleResult
            )
            .signInWithAppleButtonStyle(
                colorScheme == .dark ? .white : .black
            )
            .frame(height: 50)
            .clipShape(RoundedRectangle(cornerRadius: 12))
            .disabled(isLoading)
            .opacity(isLoading ? 0.6 : 1)

            if isLoading {
                ProgressView()
                    .tint(.secondary)
            }

            if let errorMessage {
                Text(errorMessage)
                    .font(.caption)
                    .foregroundStyle(.red)
                    .multilineTextAlignment(.center)
            }
        }
    }

    private func configureRequest(_ request: ASAuthorizationAppleIDRequest) {
        let nonce = NonceGenerator.randomNonce()
        currentNonce = nonce
        request.requestedScopes = [.fullName, .email]
        request.nonce = NonceGenerator.sha256(nonce)
    }

    private func handleResult(_ result: Result<ASAuthorization, Error>) {
        switch result {
        case .success(let authorization):
            guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential,
                  let identityTokenData = appleIDCredential.identityToken,
                  let identityToken = String(data: identityTokenData, encoding: .utf8),
                  let nonce = currentNonce else {
                errorMessage = "Failed to get credentials from Apple."
                return
            }

            // Extract name (only available on FIRST sign-in)
            let fullName = [
                appleIDCredential.fullName?.givenName,
                appleIDCredential.fullName?.familyName
            ].compactMap { $0 }.joined(separator: " ")

            let email = appleIDCredential.email  // Also only on first sign-in

            Task {
                await exchangeTokenWithSupabase(
                    identityToken: identityToken,
                    nonce: nonce,
                    fullName: fullName.isEmpty ? nil : fullName,
                    email: email
                )
            }

        case .failure(let error):
            if (error as NSError).code == ASAuthorizationError.canceled.rawValue {
                // User cancelled — not an error
                return
            }
            errorMessage = "Sign in failed: \(error.localizedDescription)"
        }
    }

    private func exchangeTokenWithSupabase(
        identityToken: String,
        nonce: String,
        fullName: String?,
        email: String?
    ) async {
        isLoading = true
        errorMessage = nil

        do {
            // Exchange the Apple identity token for a Supabase session
            try await SupabaseManager.shared.client.auth.signInWithIdToken(
                credentials: .init(
                    provider: .apple,
                    idToken: identityToken,
                    nonce: nonce
                )
            )

            // If we got a name, update the user's metadata
            if let fullName {
                try await SupabaseManager.shared.client.auth.update(
                    user: .init(data: ["full_name": .string(fullName)])
                )
            }

            await MainActor.run {
                onSuccess()
            }
        } catch {
            await MainActor.run {
                errorMessage = "Authentication failed: \(error.localizedDescription)"
            }
        }

        isLoading = false
    }
}

A few things to call out in this code. The .signIn parameter on the button determines the label text — Apple offers .signIn ("Sign in with Apple"), .signUp ("Sign up with Apple"), and .continue ("Continue with Apple"). In my experience, .continueworks best for apps where the user might already have an account — it is less committal.

Also notice that we handle the ASAuthorizationError.canceled case separately. This fires when the user dismisses the Apple sheet without signing in. It is not an error — do not show an error message for this.

4c. The Supabase Token Exchange

The signInWithIdToken method on Supabase's Swift client does the heavy lifting. It sends the Apple identity token and nonce to your Supabase project, where the server validates the token against Apple's public keys, verifies the nonce, and either creates a new user or logs in an existing one. The result is a Supabase session with access and refresh tokens.

Here is the minimal Supabase manager that supports this flow:

// Services/SupabaseManager.swift
import Supabase
import Foundation

final class SupabaseManager: Sendable {
    static let shared = SupabaseManager()

    let client: SupabaseClient

    private init() {
        client = SupabaseClient(
            supabaseURL: URL(string: "https://YOUR_PROJECT.supabase.co")!,
            supabaseKey: "YOUR_ANON_KEY"
        )
    }
}

What Data Apple Provides — And When

This is where most developers get burned. Apple's data sharing behavior is not what you would expect. Here is the complete breakdown:

Data FieldFirst Sign-InSubsequent Sign-InsNotes
User ID (sub)Always providedAlways providedStable, unique per app. Looks like 001234.abcdef1234567890.0123
Identity Token (JWT)Always providedAlways providedContains sub, email, nonce hash. Expires in 10 minutes.
EmailProvided (if user shares)NOT providedMay be real email or private relay. Stored in JWT on first auth only.
Full NameProvided (if user shares)NOT providedUser can edit the name Apple sends. Components may be nil individually.
Authorization CodeAlways providedAlways providedSingle-use code for server-side token exchange. Expires in 5 minutes.
Real User StatusProvidedUnsupported (returns .unknown)Anti-fraud signal: .likelyReal, .unknown, or .unsupported

The critical takeaway: Apple only sends the user's name and email on the very first authorization. If your app crashes before saving this data, or if your network request fails, that data is gone. You will not get it again unless the user goes to Settings → Apple ID → Password & Security → Apps Using Apple ID, revokes your app, and signs in again.

This is why you should persist the name and email immediately upon receiving them — before making any network calls. Save them to UserDefaults or a local database, then send them to your backend. If the backend call fails, you can retry later with the locally stored data.

Common Mistakes and Gotchas

In my experience building SIWA into multiple production apps, these are the issues that waste the most developer time:

Gotcha 1: Testing in Simulator

Sign in with Apple works in the iOS Simulator, but with limitations. You must be signed into an Apple ID in the Simulator (Settings → Sign in to your iPhone). The biometric prompt is skipped in the Simulator — it uses your macOS password instead. The realUserStatus always returns.unknown in the Simulator. Always test the full flow on a real device before submitting to the App Store.

Gotcha 2: Handling Credential Revocation

Users can revoke your app's access to their Apple ID at any time via Settings. When they do, your app should detect this and sign the user out. Here is how to check credential state on app launch:

// Check credential state on app launch
func checkAppleCredentialState() async {
    guard let userID = UserDefaults.standard.string(forKey: "appleUserID") else {
        return
    }

    let provider = ASAuthorizationAppleIDProvider()
    do {
        let state = try await provider.credentialState(forUserID: userID)
        switch state {
        case .authorized:
            break  // User is still authorized
        case .revoked:
            // User revoked access — sign them out
            try await SupabaseManager.shared.client.auth.signOut()
            UserDefaults.standard.removeObject(forKey: "appleUserID")
        case .notFound:
            // No credential found — sign them out
            try await SupabaseManager.shared.client.auth.signOut()
        case .transferred:
            // App was transferred to a new developer
            // Re-authenticate the user
            break
        @unknown default:
            break
        }
    } catch {
        print("Failed to check credential state: \(error)")
    }
}

You should also register for the ASAuthorizationAppleIDProvider.credentialRevokedNotificationnotification to detect revocation while the app is running:

// In your App struct or AppDelegate
NotificationCenter.default.addObserver(
    forName: ASAuthorizationAppleIDProvider.credentialRevokedNotification,
    object: nil,
    queue: .main
) { _ in
    // Sign the user out immediately
    Task {
        try? await SupabaseManager.shared.client.auth.signOut()
    }
}

Gotcha 3: The "Sign in with Apple" Button Guidelines

Apple has specific Human Interface Guidelines for the SIWA button. If you use a custom button instead of the native SignInWithAppleButton, you must follow Apple's design requirements exactly — including minimum size, corner radius, font, and spacing. My strong recommendation: just use the native button. It is less work, guaranteed to pass App Review, and automatically adapts to the user's locale and accessibility settings.

Gotcha 4: Private Relay Email Forwarding

If a user chooses "Hide My Email," Apple generates a private relay address. For your app to send emails to this address (welcome emails, receipts, password resets), you must register your outbound email domains in the Apple Developer Portal under Services → Sign in with Apple for Email Communication. Add both your domain (e.g., yourdomain.com) and your email provider's sending domain. Without this, your emails will silently bounce.

Gotcha 5: Multiple Apps, Same User

The Apple user ID (sub claim) is unique per development team, not per app. This means if you have multiple apps under the same team, the same user gets the same Apple user ID across all of them. This is actually useful for cross-app features, but it can be surprising if you did not expect it.

Testing Checklist

Before submitting your app, walk through every item on this list. I have been burned by each of these at least once:

  1. Fresh sign-in: Create a new Apple ID (or use a test account) and verify that name and email are captured correctly.
  2. Repeat sign-in: Sign in again with the same account. Verify the app handles the missing name/email gracefully.
  3. Email hiding: Test with "Hide My Email" selected. Verify the private relay email is stored and that you can send emails to it.
  4. Cancellation: Tap the Sign in with Apple button, then dismiss the sheet. Verify no error is shown.
  5. Credential revocation: Go to Settings → Apple ID → Password & Security → Apps Using Apple ID → Stop Using Apple ID. Reopen your app and verify the user is signed out.
  6. Network failure: Enable Airplane Mode after Apple's sheet but before the Supabase call. Verify the error is handled gracefully and the user can retry.
  7. Real device: Test the entire flow on a physical iPhone, not just the Simulator.
  8. Dark mode and light mode: Verify the button looks correct in both color schemes.
  9. iPad and different screen sizes: Verify the layout adapts correctly.
  10. VoiceOver: Verify the button is accessible and announced correctly.

Production Architecture Tips

A few things I have learned from running SIWA in production apps with tens of thousands of users:

Store the Apple User ID Immediately

Save appleIDCredential.user to UserDefaults or Keychain the moment you receive it. You need this for credential state checks on every app launch. Do not rely solely on your backend having it — the credential check happens before any network call.

Always Offer Account Deletion

As of iOS 16, Apple requires apps that support account creation to also support account deletion. If you offer Sign in with Apple, you must provide a way to delete the account. When deleting, you should also revoke the Apple token using Apple's revocation endpoint to be a good citizen:

// Revoke Apple token on account deletion
func revokeAppleToken(authorizationCode: String) async throws {
    // Send the authorization code to your backend
    // Your backend calls: POST https://appleid.apple.com/auth/revoke
    // with client_id, client_secret, token, and token_type_hint
    let url = URL(string: "https://your-backend.com/auth/apple/revoke")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONEncoder().encode(["code": authorizationCode])

    let (_, response) = try await URLSession.shared.data(for: request)
    guard let httpResponse = response as? HTTPURLResponse,
          httpResponse.statusCode == 200 else {
        throw AuthError.revocationFailed
    }
}

Handle the "Transferred" Credential State

If your app is transferred to a different development team (e.g., you sell your app), existing Apple user IDs will change. The .transferred credential state lets you detect this and re-authenticate users. This is an edge case, but it is important to handle if you ever plan to sell or transfer your app.

Skip the Setup — Use The Swift Kit

If this tutorial felt like a lot of steps, that is because it is. Between the Xcode capabilities, the Developer Portal configuration, the nonce generation, the credential handling, the revocation observer, and the dozen gotchas — Sign in with Apple is a solid day of work to implement correctly. And that is if you already know what you are doing.

The Swift Kit includes Sign in with Apple fully pre-wired with Supabase. The nonce generation, token exchange, credential state checking, revocation handling, and account deletion are all implemented and tested. You add your Apple Developer credentials to the Supabase dashboard, and it works. No guessing which nonce goes where. No losing user names on the first sign-in. Check the features page for the complete authentication module, or visit pricing to get started.

Share this article

Ready to ship your iOS app faster?

The Swift Kit gives you a production-ready SwiftUI codebase with onboarding, paywalls, auth, AI integrations, and more. Stop building boilerplate. Start building your product.

Get The Swift Kit