I have been working on an iOS app for some time now and wanted to rely on just social logins for authentication. Firebase was the clear choice for me since some of the media that the app accesses is stored in Google Cloud Storage, and so it works great since they are part of the same ecosystem.
If you haven’t checked it out already, I also have a post on “Implementing Google Sign-in in a SwiftUI app using the Firebase SDK“. Do check that out as well if you want to cover two of the most widely used social login methods.
In this post, I will show you a clean way to implement “Sign in with Apple” in a SwiftUI app using the Firebase SDK. Again, I say clean since we will use the Clean Architecture principles of Robert C. Martin to ensure that we decouple all layers. We will be using MVVM for this integration.
Without further ado, let’s begin!
1. Create a SwiftUI app
Set up a fresh SwiftUI app (or use an existing one).

2. Add Dependencies
The next step is to add the dependencies that we need for this implementation. You could use Swift Package Manager or Cocoapods for this, but for the purpose of this blog post, I will use Swift Package Manager here.
- Firebase SDK – https://github.com/firebase/firebase-ios-sdk
3. Create a Firebase Project and add its configuration to the SwiftUI project



4. Create the AuthService protocol
Although Apple handles everything related to authenticating the user with their Apple ID, after signing in we still need to send the ID token to Firebase to create a record in Firebase Auth.
We will achieve this by creating an authentication service that handles all different sign-in methods (like Apple, Google, Facebook, etc.). To access/trigger the methods from this service from the UI, we would create a view model that acts as a bridge between the view and the authentication service.
Let us now define the protocol for this authentication service.
import Foundation
import AuthenticationServices
import FirebaseAuth
/// An enumeration representing logged in status.
enum AuthState: Equatable {
/// The user is logged in and is the associated value of this case.
case signedIn(User)
/// The user is logged out.
case signedOut
}
protocol AuthService {
/// Restores current user session from keychain if valid
func restorePreviousSignIn() async -> AuthState
/// Configures the Apple sign-in authorization request
func configure(appleSignInAuthorizationRequest: ASAuthorizationAppleIDRequest)
/// Signs in user with the Apple sign-in
func signInWithApple(requestAuthorizationResult: Result<ASAuthorization, Error>) async -> AuthState
/// Signs in to firebase using the given auth credentials
/// - note: To be invoked after authenticating with username-password or social sign-in
func signOut() -> Bool
}
We have a few things to cover in the above protocol:
- AuthState – This is an enum that has cases of either being
signedIn
or beingsignedOut
. In case it issignedIn
, it also has a FirebaseUser
object associated with it. - restorePreviousSignIn() – This method basically checks the user defaults to see if there is an existing Firebase user object that is stored and returns an AuthState depending on it.
- configure(appleSignInAuthorizationRequest:) – This method is used to configure the Apple ID request object that is sent to Apple when the user clicks on the “Sign in with Apple” button.
- signInWithApple(requestionAuthorizationResult:) – This method handles the authorization result from Apple after it has processed the Apple ID request. Here we would be unwrapping the result to get the ID token, which we would be sending to Firebase to create a record in Firebase Auth. This method will return the final
AuthState
depending on the result. - signOut() – This method is used to sign out the user by removing the access tokens on the device storage.
5. Create FirebaseAuthService that implements AuthService
Now that we know what methods the view model would need from the auth service let us create an auth service using Firebase.
import Foundation
import FirebaseAuth
import AuthenticationServices
import CryptoKit
class FirebaseAuthService: AuthService {
public static let shared = FirebaseAuthService()
private init() {}
// MARK: - Properties
// Unhashed nonce.
private var currentNonce: String?
// MARK: - Methods
/// Restores current user session from keychain if valid
func restorePreviousSignIn() async -> AuthState {
return await withCheckedContinuation { continuation in
if let currentUser = Auth.auth().currentUser {
AppLogger.debug("Restoring previous sign in")
continuation.resume(returning: .signedIn(currentUser))
} else {
AppLogger.debug("Could not restore previous sign in")
continuation.resume(returning: .signedOut)
}
}
}
/// Configures the Apple sign-in authorization request
func configure(appleSignInAuthorizationRequest request: ASAuthorizationAppleIDRequest) {
let nonce = randomNonceString()
currentNonce = nonce
request.requestedScopes = [.fullName, .email]
request.nonce = sha256(nonce)
}
/// Uses authorization credentials for from Apple sign-in to sign in on Firebase
func signInWithApple(requestAuthorizationResult result: Result<ASAuthorization, Error>) async -> AuthState {
switch result {
case let .success(authorization):
switch authorization.credential {
case let appleIDCredential as ASAuthorizationAppleIDCredential:
// Create an account in your system.
let userIdentifier = appleIDCredential.user
let fullName = String(describing: appleIDCredential.fullName)
let email = String(describing: appleIDCredential.email)
AppLogger.debug("Signed in with Apple: \(userIdentifier), \(fullName), \(email)")
guard let nonce = currentNonce else {
AppLogger.error("Invalid state: A login callback was received, but no login request was sent.")
return .signedOut
}
guard let idToken = appleIDCredential.identityToken else {
AppLogger.error("Unable to fetch identity token")
return .signedOut
}
guard let idTokenString = String(data: idToken, encoding: .utf8) else {
AppLogger.error("Unable to serialize token string from data: \(idToken.debugDescription)")
return .signedOut
}
// Initialize a Firebase credential.
let credential = OAuthProvider.credential(
withProviderID: "apple.com",
idToken: idTokenString,
rawNonce: nonce
)
return await signInOnFirebase(with: credential)
default:
AppLogger.error("Unexpected credential error while signing in with Apple")
return .signedOut
}
case let .failure(error):
AppLogger.error("Unexpectedly got error while signing in with Apple: \(error)")
return .signedOut
}
}
/// Signs in to firebase using the given auth credentials
/// - note: To be invoked after authenticating with username-password or social sign-in
private func signInOnFirebase(with credential: AuthCredential) async -> AuthState {
do {
let authDataResult = try await Auth.auth().signIn(with: credential)
let user = authDataResult.user
return .signedIn(user)
} catch {
AppLogger.error("Unexpectedly got error while signing in with firebase: \(error)")
return .signedOut
}
}
/// Signs in user out of all authenticators and firebase
func signOut() -> Bool {
do {
try Auth.auth().signOut()
return true
} catch {
AppLogger.error("Encountered error signing out: \(error).")
return false
}
}
}
// MARK: - Crytographic algorithms
extension FirebaseAuthService {
/// Generates a random nonce string
/// - note: Adapted from https://auth0.com/docs/api-auth/tutorials/nonce#generate-a-cryptographically-random-nonce
private func randomNonceString(length: Int = 32) -> String {
precondition(length > 0)
let charset: [Character] =
Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
var result = ""
var remainingLength = length
while remainingLength > 0 {
let randoms: [UInt8] = (0 ..< 16).map { _ in
var random: UInt8 = 0
let errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random)
if errorCode != errSecSuccess {
fatalError(
"Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)"
)
}
return random
}
randoms.forEach { random in
if remainingLength == 0 {
return
}
if random < charset.count {
result.append(charset[Int(random)])
remainingLength -= 1
}
}
}
return result
}
/// Hash using SHA256
private func sha256(_ input: String) -> String {
let inputData = Data(input.utf8)
let hashedData = SHA256.hash(data: inputData)
let hashString = hashedData.compactMap {
String(format: "%02x", $0)
}.joined()
return hashString
}
}
Notice that apart from the methods in the AuthService protocol that we have implemented, there are also two private methods, randomNonceString(length:)
and sha256(_:)
that have been created.
Unlike Google sign-in, in order to integrate Apple sign-in with Firebase, we would need to provide a hashed nonce to the Apple ID request, and also be sent to Firebase along with the ID token that we get from Apple. Firebase uses this nonce to ensure that the Apple ID authorization result is in fact from an Apple ID request that was made.
6. Create the AuthenticationViewModel
It is time for us to create the AuthenticationViewModel
from where we would be invoking the AuthService
.
import Foundation
import FirebaseAuth
import AuthenticationServices
/// A class conforming to `ObservableObject` used to represent a user's authentication status.
@MainActor
final class AuthenticationViewModel: ObservableObject {
/// The user's log in status.
/// - note: This will publish updates when its value changes.
@Published var state: AuthState
private var authService: AuthService {
return FirebaseAuthService.shared
}
/// Creates an instance of this view model.
init() {
self.state = .signedOut
}
func restorePreviousSignIn() {
Task { [weak self] in
if state == .signedOut {
let state = await authService.restorePreviousSignIn()
if case let .signedIn(user) = state {
// Set the user session in your app here if needed
}
self?.state = state
}
}
}
/// Configures the Apple sign-in authorization request
func configure(appleSignInAuthorizationRequest request: ASAuthorizationAppleIDRequest) {
return authService.configure(appleSignInAuthorizationRequest: request)
}
/// Signs the user in with Apple
func signInWithApple(requestAuthorizationResult result: Result<ASAuthorization, Error>) {
Task { [weak self] in
let state = await authService.signInWithApple(requestAuthorizationResult: result)
if case let .signedIn(user) = state {
// Set the user session in your app here if needed
}
self?.state = state
}
}
/// Signs the user out.
func signOut() {
Task {
await MainActor.run {
if authService.signOut() {
self.state = .signedOut
}
}
}
}
/// Returns the user object if in `signedIn` state
func getUser() -> User? {
guard case let .signedIn(user) = state else {
return nil
}
return user
}
}
We are making this view model a class that conforms to the ObservableObject
protocol. This allows us to set @Published
to the property state
(of type AuthState
) in the view model. Doing this would publish the changes in this property to wherever it is being used, which allows us to manipulate the view being displayed.
7. Setup the app’s root view
This is an important part of the app. Not only should we set the root view here, but also the app’s global settings.
When using Firebase, we would have to configure the Firebase project in the UIApplicationDelegate. However, in SwiftUI, we don’t have a UIApplicationDelegate. An official workaround for this is to use to @UIApplicationDelegateAdaptor
like below.
We also need to create an instance of the AuthenticationViewModel and set it as a @StateObject
. This allows the view model to maintain a global state. We also need to pass this view model as an environment object to the ContentView
.
import SwiftUI
import FirebaseCore
@main
struct AppleSignInApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject var authViewModel = AuthenticationViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(authViewModel)
.onAppear {
authViewModel.restorePreviousSignIn()
}
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
) -> Bool {
// Configuring Firebase
FirebaseApp.configure()
return true
}
}
8. Create the ContentView and SignInView
The ContentView view in our app determines whether to show the SignInView or an authenticated view.
The first thing to do here is to claim our environment variable, which we have passed from the AppleSignInApp
to the ContentView
. The step after is to determine which view to display based on the state
property of the AuthenticationViewModel
. Here is how we do it:
import SwiftUI
struct ContentView: View {
@EnvironmentObject var authViewModel: AuthenticationViewModel
var body: some View {
return Group {
switch authViewModel.state {
case let .signedIn(user):
Text("\(user.displayName) is Signed In")
case .signedOut:
SignInView()
}
}
.navigationBarTitle("")
.navigationBarHidden(true)
.onAppear {
self.isNavigationBarHidden = true
}
}
}
Once the ContentView
is set up, we finally have our SignInView
to be created, which would invoke the configure(appleSignInAuthorizationRequest:)
and signInWithApple(requestAuthorizationResult:)
methods of our AuthenticationViewModel
. With iOS 14 and above SwiftUI comes with a native “Sign in with Apple” button called SignInWithAppleButton
. It takes in 2 closures, one to configure the ASAuthorizationAppleIdRequest
if needed, and the other to handle the authentication result.
import SwiftUI
import AuthenticationServices
struct SignInView: View {
@EnvironmentObject var authViewModel: AuthenticationViewModel
var body: some View {
VStack {
SignInWithAppleButton(.signIn) { aSAuthorizationAppleIdRequest in
authViewModel.configure(appleSignInAuthorizationRequest: aSAuthorizationAppleIdRequest)
} onCompletion: { result in
authViewModel.signInWithApple(requestAuthorizationResult: result)
}
.signInWithAppleButtonStyle(.white)
.frame(height: 45)
.padding(.horizontal)
.padding(.bottom)
Spacer()
}
}
}
9. Time to run our app
And that’s all! We have finally implemented Apple Sign-In in our SwiftUI app using the Firebase SDK. We can also verify that the user has in fact been added to Firebase by checking the console.
Hope this was useful to somebody looking to implement Apple Sign-in on their app. See you again with another blog post.
Until then, tschüss!