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_ididentityToken
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_idnever changes - The
account_idis also available in the JWT token as thesubclaim
Key Takeaway
Always use
account_idas 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
- Open your Xcode project
- Select your app target
- Go to Signing & Capabilities
- Click + Capability
- 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
.fullNameand.email - Use
UIViewRepresentableto 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
- 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_ididentity_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
- Run app
- Tap Sign in with Apple
- Complete auth
Expected logs:
Fresh from Apple
Cached new user data
Subsequent Login Test
- Force quit app
- Login again
Expected logs:
nil [Fresh from Apple]
using cached data
Hide My Email Test
Sign in
Go to:
Settings -> Sign In with AppleChange email preference
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