Blog

Implementing Sign in with Apple in SwiftUI: A Complete Guide

May 25, 2026 · 10 min read


Sign in with Apple is more than just another authentication option — it’s a requirement for apps that offer third-party login options. But beyond compliance, it offers a seamless, privacy-focused authentication experience that your users will appreciate.

In this guide, we’ll walk through implementing Sign in with Apple in a SwiftUI app, handling the unique challenges it presents, and building a robust solution that works reliably.

Reading time: ~10 minutes


Why Sign in with Apple is Different

Unlike traditional OAuth providers that send user information with every login, Apple takes a privacy-first approach.

When a user signs in with Apple for the first time, you receive their email and name.

But here’s the catch:

Apple will only send this information once.

On subsequent logins, you’ll only receive:

  • account_id
  • identityToken

No email. No name.

This design decision protects user privacy but creates a challenge:

You must cache user information on the first login to avoid showing “Unknown User” on subsequent logins.


Understanding “Hide My Email”

Apple’s Hide My Email feature adds another layer of complexity.

Users can choose to:

  • Share their real email

  • Use Apple’s private relay email Example:

    xyz123@privaterelay.appleid.com
    

Important Notes for Developers

  • Users can change their email preference after initial sign-in
  • Apple might send a different email later
  • The account_id never changes
  • The account_id is also available in the JWT token as the sub claim

Key Takeaway

Always use account_id as your primary user identifier — not email.

Email is supplementary user information that may change.


Before You Start

To implement Sign in with Apple, you need:

  • Apple Developer Program membership
  • Xcode project setup
  • Sign in with Apple capability enabled

Adding the Capability

  1. Open your Xcode project
  2. Select your app target
  3. Go to Signing & Capabilities
  4. Click + Capability
  5. Add Sign in with Apple

Implementation Steps

Step 1: Create the Apple Sign-In Button

First, create a reusable SwiftUI component wrapping the native ASAuthorizationAppleIDButton.

//
//  AppleSignInButton.swift
//  YourApp
//

import SwiftUI
import AuthenticationServices

struct AppleSignInButton: View {

    let onSuccess: (ASAuthorizationAppleIDCredential) -> Void
    let onFailure: (Error) -> Void

    var body: some View {
        SignInWithAppleButtonView(
            onSuccess: onSuccess,
            onFailure: onFailure
        )
        .frame(height: 54)
        .cornerRadius(16)
    }
}

struct SignInWithAppleButtonView: UIViewRepresentable {

    let onSuccess: (ASAuthorizationAppleIDCredential) -> Void
    let onFailure: (Error) -> Void

    func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {

        let button = ASAuthorizationAppleIDButton(
            authorizationButtonType: .signIn,
            authorizationButtonStyle: .white
        )

        button.cornerRadius = 16

        button.addTarget(
            context.coordinator,
            action: #selector(Coordinator.handleAppleSignIn),
            for: .touchUpInside
        )

        return button
    }

    func updateUIView(
        _ uiView: ASAuthorizationAppleIDButton,
        context: Context
    ) {}

    func makeCoordinator() -> Coordinator {
        Coordinator(
            onSuccess: onSuccess,
            onFailure: onFailure
        )
    }

    class Coordinator: NSObject,
                       ASAuthorizationControllerDelegate,
                       ASAuthorizationControllerPresentationContextProviding {

        let onSuccess: (ASAuthorizationAppleIDCredential) -> Void
        let onFailure: (Error) -> Void

        init(
            onSuccess: @escaping (ASAuthorizationAppleIDCredential) -> Void,
            onFailure: @escaping (Error) -> Void
        ) {
            self.onSuccess = onSuccess
            self.onFailure = onFailure
        }

        @objc func handleAppleSignIn() {

            let appleIDProvider = ASAuthorizationAppleIDProvider()

            let request = appleIDProvider.createRequest()

            request.requestedScopes = [.fullName, .email]

            let authorizationController = ASAuthorizationController(
                authorizationRequests: [request]
            )

            authorizationController.delegate = self
            authorizationController.presentationContextProvider = self

            authorizationController.performRequests()
        }

        func authorizationController(
            controller: ASAuthorizationController,
            didCompleteWithAuthorization authorization: ASAuthorization
        ) {

            if let appleIDCredential =
                authorization.credential as? ASAuthorizationAppleIDCredential {

                onSuccess(appleIDCredential)
            }
        }

        func authorizationController(
            controller: ASAuthorizationController,
            didCompleteWithError error: Error
        ) {
            onFailure(error)
        }

        func presentationAnchor(
            for controller: ASAuthorizationController
        ) -> ASPresentationAnchor {

            guard let window = UIApplication.shared.connectedScenes
                .compactMap({ $0 as? UIWindowScene })
                .flatMap({ $0.windows })
                .first(where: { $0.isKeyWindow }) else {

                return UIWindow()
            }

            return window
        }
    }
}

Key Points

  • Request both .fullName and .email
  • Use UIViewRepresentable to bridge UIKit
  • Use the Coordinator pattern for delegation

Step 2: Build the Cache Layer

This is the most critical part.

Apple only provides user information once.

You must cache it locally.

//
//  AppleSignInCache.swift
//  YourApp
//

import Foundation

/// Persistent cache for Apple Sign In credentials
/// Stores user data that Apple only provides on first sign-in
class AppleSignInCache {

    private static let userDefaults = UserDefaults.standard

    private enum Keys {
        static let accountId = "apple_account_id"
        static let email = "apple_email"
        static let firstName = "apple_first_name"
        static let lastName = "apple_last_name"
        static let cachedAt = "apple_cached_at"
    }

    // MARK: - Save Credentials

    static func save(
        accountId: String,
        email: String?,
        firstName: String?,
        lastName: String?
    ) {

        userDefaults.set(accountId, forKey: Keys.accountId)
        userDefaults.set(email, forKey: Keys.email)
        userDefaults.set(firstName, forKey: Keys.firstName)
        userDefaults.set(lastName, forKey: Keys.lastName)
        userDefaults.set(Date(), forKey: Keys.cachedAt)

        userDefaults.synchronize()

        print("✅ Cached Apple Sign In data for accountId: \(accountId)")
    }

    // MARK: - Retrieve Credentials

    static func getCredentials(
        for accountId: String
    ) -> (
        email: String?,
        firstName: String?,
        lastName: String?
    )? {

        let cachedAccountId = userDefaults.string(
            forKey: Keys.accountId
        )

        guard cachedAccountId == accountId else {

            print("""
            ⚠️ AccountId mismatch
            cached: \(cachedAccountId ?? "nil")
            requested: \(accountId)
            """)

            return nil
        }

        return (
            email: userDefaults.string(forKey: Keys.email),
            firstName: userDefaults.string(forKey: Keys.firstName),
            lastName: userDefaults.string(forKey: Keys.lastName)
        )
    }

    // MARK: - Clear Cache

    static func clear() {

        userDefaults.removeObject(forKey: Keys.accountId)
        userDefaults.removeObject(forKey: Keys.email)
        userDefaults.removeObject(forKey: Keys.firstName)
        userDefaults.removeObject(forKey: Keys.lastName)
        userDefaults.removeObject(forKey: Keys.cachedAt)

        userDefaults.synchronize()

        print("🗑️ Cleared Apple Sign In cache")
    }

    // MARK: - Utilities

    static func hasCachedCredentials() -> Bool {
        return userDefaults.string(forKey: Keys.accountId) != nil
    }
}

Important Security Notes

❌ Never Cache

  • Identity token
  • Provider access token

✅ Safe to Cache

  • Account ID
  • Email
  • Name

Step 3: Create the ViewModel

The ViewModel orchestrates:

  • Authentication flow
  • Cache handling
  • Backend communication
//
//  LoginViewModel.swift
//  YourApp
//

import Foundation
import SwiftUI
import AuthenticationServices

class LoginViewModel: ObservableObject {

    @Published var isLoading: Bool = false
    @Published var errorMessage: String?

    // MARK: - Apple Sign In Success Handler

    func handleAppleSignInSuccess(
        _ credential: ASAuthorizationAppleIDCredential
    ) {

        isLoading = true
        errorMessage = nil

        // Permanent unique identifier
        let accountId = credential.user

        let authProvider = "3"

        // Identity token
        let identityToken = credential.identityToken.flatMap {
            String(data: $0, encoding: .utf8)
        }

        guard let identityToken = identityToken else {

            DispatchQueue.main.async {
                self.isLoading = false
                self.errorMessage = "Failed to retrieve identity token"
            }

            return
        }

        // Fresh data from Apple
        let credentialEmail = credential.email
        let credentialFirstName = credential.fullName?.givenName
        let credentialLastName = credential.fullName?.familyName

        // Cached data
        let cachedData = AppleSignInCache.getCredentials(
            for: accountId
        )

        // Priority:
        // Fresh Apple data -> Cached data
        let finalEmail =
            credentialEmail ?? cachedData?.email

        let finalFirstName =
            credentialFirstName ?? cachedData?.firstName

        let finalLastName =
            credentialLastName ?? cachedData?.lastName

        print("🍎 Apple Sign In Data:")
        print("AccountId: \(accountId)")
        print("Final Email: \(finalEmail ?? "nil")")

        // Cache new data
        if credentialEmail != nil ||
            credentialFirstName != nil ||
            credentialLastName != nil {

            AppleSignInCache.save(
                accountId: accountId,
                email: credentialEmail,
                firstName: credentialFirstName,
                lastName: credentialLastName
            )

            print("💾 Cached new user data from Apple")
        }

        // Backend API call
        AuthRepo.socialLogin(
            accountId: accountId,
            authProvider: authProvider,
            providerAccessToken: identityToken,
            firstName: finalFirstName,
            lastName: finalLastName,
            email: finalEmail,

            success: { [weak self] token, user in

                DispatchQueue.main.async {

                    self?.isLoading = false

                    TokenManager.saveToken(token)
                    TokenManager.saveUserId(user.id)

                    NavigationCoordinator.shared
                        .switchStartPoint(.home)
                }
            },

            failed: { [weak self] message in

                DispatchQueue.main.async {

                    self?.isLoading = false

                    self?.errorMessage =
                        message ?? "Apple Sign In failed"
                }
            }
        )
    }

    // MARK: - Failure Handler

    func handleAppleSignInFailure(_ error: Error) {

        DispatchQueue.main.async {

            self.isLoading = false

            if let authError = error as? ASAuthorizationError {

                switch authError.code {

                case .canceled:
                    print("⚠️ User canceled Apple Sign In")
                    return

                case .failed:
                    self.errorMessage =
                        "Apple authorization failed"

                case .invalidResponse:
                    self.errorMessage =
                        "Invalid response from Apple"

                case .notHandled:
                    self.errorMessage =
                        "Request could not be handled"

                case .unknown:
                    self.errorMessage =
                        "Unknown error occurred"

                @unknown default:
                    self.errorMessage =
                        "Apple Sign In error"
                }

            } else {

                self.errorMessage =
                    "Apple Sign In error: \(error.localizedDescription)"
            }
        }
    }
}

Key Implementation Details

Account ID Priority

account_id is the source of truth.

It never changes.


Data Priority

Fresh Apple Data -> Cached Data

Caching Strategy

Only cache when Apple provides new data.


Error Handling

Do not show errors for cancellation.


Step 4: Integrate Into Login Screen

//
//  LoginScreen.swift
//  YourApp
//

import SwiftUI
import AuthenticationServices

struct LoginScreen: View {

    @StateObject private var viewModel = LoginViewModel()

    var body: some View {

        ZStack {

            Color.black
                .ignoresSafeArea()

            VStack(spacing: 32) {

                Spacer()
                    .frame(height: 44)

                // Logo
                Image("logo")
                    .resizable()
                    .scaledToFit()
                    .frame(height: 120)
                    .padding(.top, 16)

                Spacer()
                    .frame(height: 30)

                // Your email/password fields here

                if let error = viewModel.errorMessage {

                    Text(error)
                        .foregroundColor(.red)
                        .font(
                            .system(
                                size: 14,
                                weight: .semibold
                            )
                        )
                        .padding(.horizontal, 24)
                        .multilineTextAlignment(.center)
                }

                AppleSignInButton(

                    onSuccess: { credential in
                        viewModel
                            .handleAppleSignInSuccess(
                                credential
                            )
                    },

                    onFailure: { error in
                        viewModel
                            .handleAppleSignInFailure(
                                error
                            )
                    }
                )
                .padding(.horizontal, 24)

                Spacer()
            }
        }
    }
}

#Preview {
    LoginScreen()
}

Backend Integration Overview

Your backend receives:

  • account_id
  • identity_token
  • optional email
  • optional first_name
  • optional last_name

Backend Responsibilities

1. Verify JWT Signature

Using Apple public keys:

https://appleid.apple.com/auth/keys

2. Validate

  • Token expiration

  • Issuer:

    https://appleid.apple.com
    

3. Extract sub

The sub claim matches the account_id.

4. Login or Register

  • Existing user → Login
  • New user → Create account

5. Return App Token

Your backend returns your own authentication token.


Best Practices

1. Use account_id as Primary Identifier

let accountId = credential.user

2. Always Cache on First Login

if credentialEmail != nil ||
   credentialFirstName != nil {

    AppleSignInCache.save(...)
}

3. Never Cache Identity Tokens

// ❌ NEVER cache identity tokens

// ✅ Send directly to backend

4. Handle Cancellation Gracefully

case .canceled:
    return

5. Provide Fallback Names

let displayName =
    firstName ??
    cachedFirstName ??
    "User"

Common Pitfalls

❌ Using Email as Identifier

Problem: Email may change Solution: Use account_id


❌ Not Caching User Data

Problem: Missing names on future logins Solution: Cache immediately


❌ Caching Identity Tokens

Problem: Security vulnerability Solution: Never store tokens locally


❌ Showing Errors on Cancellation

Problem: Poor UX Solution: Handle silently


❌ Forgetting Cache Cleanup

Problem: Old user data persists Solution:

AppleSignInCache.clear()

Testing Your Implementation

First-Time Login Test

  1. Run app
  2. Tap Sign in with Apple
  3. Complete auth

Expected logs:

Fresh from Apple
Cached new user data

Subsequent Login Test

  1. Force quit app
  2. Login again

Expected logs:

nil [Fresh from Apple]
using cached data

Hide My Email Test

  1. Sign in

  2. Go to:

    Settings -> Sign In with Apple
    
  3. Change email preference

  4. Sign in again

Expected:

  • Backend still recognizes user via account_id

Conclusion

Sign in with Apple’s privacy-first approach requires careful handling of user data.

Key Takeaways

✅ Use account_id as primary identifier ✅ Cache user data immediately ✅ Never cache identity tokens ✅ Always check cache on future logins ✅ Backend must verify Apple JWTs

By implementing a robust caching strategy and respecting Apple’s privacy model, you can build a seamless and secure authentication experience.


Resources

  • Apple Sign In Documentation
  • Hide My Email Feature
  • Human Interface Guidelines
  • AuthenticationServices Framework