Firebase Authentication in SwiftUI
Anonymous Authentication
Implementing guest accounts with Firebase Anonymous Authentication
Most apps now need to securely authenticate user’s identity, and save user’s data in the cloud and provide the same personalized experience across all of the user’s devices (across different platforms too).
Firebase Authentication provides backend services, easy-to-use SDKs, and ready-made UI libraries to authenticate users to your app. It supports authentication using passwords, phone numbers, popular federated identity providers like Google, Facebook and Twitter, and more. ~ Firebase Documentation
In this article you will learn how to implement single sign on using Firebase Authentication through different service providers (Apple, Google, and Anonymous), and link between them.
This tutorial contains 5 parts:
- Part 1: Setup and Anonymous Authentication.
- Part 2: Google Authentication.
- Part 3: Apple Authentication.
- Part 4: Handling Link Errors.
- Part 5: Deleting User Account & Revoke Access Token.
To get started, download starter project here.
Create a Firebase project, register an iOS app, & add the Firebase SDK
Before we start implementing Firebase authentication in our app, first we need to create a Firebase project, register our iOS app, and add the Firebase SDK to our project and select libraries.
If you do not know how to add Firebase to your SwiftUI project using SPM (Swift Package Manager), follow this tutorial to Add Firebase to SwiftUI Project.
Feel free to disable Google Analytics when creating Firebase project, as it is not essential for this tutorial. Only select the FirebaseAuth library when adding the SDK.
Initialize the Firebase SDK
Open AuthLoginApp
or your @main
struct and add the following code:
import SwiftUI
import FirebaseCore
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
return true
}
}
@main
struct AuthLoginApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
init() {
// Use Firebase library to configure APIs
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
(To learn more about this code, check out the previously mentioned tutorial.)
Now that we configured Firebase, let’s implement authentication.
Add Authentication to your app
Create a new Swift file named AuthManager.swift, declare a class named AuthManager
class and conform it toObservableObject
. This class will be responsible for handling the Firebase authentication functionality in our app.
Add the following code:
import AuthenticationServices
import FirebaseAuth
import FirebaseCore
enum AuthState {
case authenticated // Anonymously authenticated in Firebase.
case signedIn // Authenticated in Firebase using one of service providers, and not anonymous.
case signedOut // Not authenticated in Firebase.
}
@MainActor
class AuthManager: ObservableObject {
@Published var user: User?
@Published var authState = AuthState.signedOut
}
Listen to the app’s authentication state
To listen to your app’s authentication state, use the addStateDidChangeListener(_:)
method to attach a listener to the auth
object. This listener will be called whenever the user’s sign-in state changes.
In AuthManager
class, add the following:
- Define
AuthStateDidChangeListenerHandle
to handle add/remove listener. - Create functions to add (attach) and remove this listener.
InsideaddStateDidChangeListener(_:)
callback, make sure to update the@Published
properties to reflect the change in your UI. - Start listening to the authentication state upon
AuthManager
initialization. - Create a function named
updateState(user:)
. This function will take aUser
and runs on theMainActor
to update theuser
andauthState
properties.
/// 1.
private var authStateHandle: AuthStateDidChangeListenerHandle!
init() {
// 3.
configureAuthStateChanges()
}
// 2.
func configureAuthStateChanges() {
authStateHandle = Auth.auth().addStateDidChangeListener { auth, user in
print("Auth changed: \(user != nil)")
self.updateState(user: user)
}
}
// 2.
func removeAuthStateListener() {
Auth.auth().removeStateDidChangeListener(authStateHandle)
}
// 4.
func updateState(user: User?) {
self.user = user
let isAuthenticatedUser = user != nil
let isAnonymous = user?.isAnonymous ?? false
if isAuthenticatedUser {
self.authState = isAnonymous ? .authenticated : .signedIn
} else {
self.authState = .signedOut
}
}
In order to access AuthManager
in your UI, pass it through the SwiftUI environment using the .environmentObject(object:)
view modifier. A good place to instantiate the AuthManager
is in your app’s main application struct, AuthLoginApp
:
@main
struct AuthLoginApp: App {
// 1. Add StateObject authManager.
@StateObject var authManager: AuthManager
init() {
FirebaseApp.configure()
// 2. Initialize authManager.
let authManager = AuthManager()
_authManager = StateObject(wrappedValue: authManager)
}
var body: some Scene {
WindowGroup {
ContentView()
// 3. Pass authManager to enviromentObject.
.environmentObject(authManager)
}
}
}
Make sure to capture the authManager
environment object in your views as follows:
@EnvironmentObject var authManager: AuthManager
Start the sign in process
Anonymous authentication
“You can use Firebase Authentication to create and use temporary anonymous accounts to authenticate with Firebase. These temporary anonymous accounts can be used to allow users who haven’t yet signed up to your app to work with data protected by security rules. If an anonymous user decides to sign up to your app, you can link their sign-in credentials to the anonymous account so that they can continue to work with their protected data in future sessions.”
From Firebase documentation ~ Authenticate with Firebase Anonymously on Apple Platforms
It’s nice to have that option if you don’t want to force the user to create an account before they can explore the app (which is recommended for public facing apps).
Open the Firebase Console, navigate to the Authentication section, and under Sign-in methods, enable Anonymous.
Go back to Xcode, and add the following function named signInAnonymously()
to the AuthManager
class.
func signInAnonymously() async throws -> AuthDataResult? {
do {
let result = try await Auth.auth().signInAnonymously()
print("FirebaseAuthSuccess: Sign in anonymously, UID:(\(String(describing: result.user.uid)))")
return result
}
catch {
print("FirebaseAuthError: failed to sign in anonymously: \(error.localizedDescription)")
throw error
}
}
In LoginView
add the following function after the body
:
func signAnonymously() {
Task {
do {
let result = try await authManager.signInAnonymously()
}
catch {
print("SignInAnonymouslyError: \(error)")
}
}
}
Replace // TODO: Sign-in Anonymously
inside the Skip
button action with a call to signAnonymously()
.
If the user skipped Login (anonymously authenticated), then do not show
LoginView
upon launch.
In ContentView
inside VStack
, replace isLoggedIn
with authManager.authState != .signedOut
, as follows:
if authManager.authState != .signedOut {
HomeView()
} else {
LoginView()
}
Run the code 📲 and tap the Skip button.
- In Authentication section of Firebase Console, under users tab, you will now find authenticated anonymous user..
If you run the app again, you will still be signed in with the same anonymous user, as Firebase keeps track of the currently signed in user.
However, keep in mind that if you sign out and then sign in again, a new anonymous user will be created, so keep that in mind when implementing Anonymous Authentication.
Sign out functionality
As mentioned before, temporary anonymous authenticated accounts will allow users who haven’t yet signed up to your app to work with data protected by security rules.
And so sign out should not be available with anonymous authentication, but we should present sign-in to the user.
- First, add
signOut()
function inAuthManager
.
func signOut() async throws {
if let user = Auth.auth().currentUser {
do {
// TODO: Sign out from signed-in Provider.
try Auth.auth().signOut()
}
catch let error as NSError {
print("FirebaseAuthError: failed to sign out from Firebase, \(error)")
throw error
}
}
}
Show the user’s name and email in HomeView
only when user is signed in (i.e. authenticated and not anonymous).
Replace the inner VStack
with the following:
VStack(alignment: .leading) {
if authManager.authState == .signedIn {
Text(authManager.user?.displayName ?? "Name placeholder")
.font(.headline)
Text(authManager.user?.email ?? "Email placeholder")
.font(.subheadline)
}
else {
Text("Sign-in to view data!")
.font(.headline)
}
}
then add the following signOut()
function after body
:
func signOut() {
Task {
do {
try await authManager.signOut()
}
catch {
print("Error: \(error)")
}
}
}
Add the following @State
variable, and replace isLoggedIn
inside Sign out
button with authManager.authState != .signedIn
. (We don’t need isLoggedIn
anymore).
@State private var showLoginSheet = false
Inside the Logout button action replace // TODO: Log out
with the following:
if authManager.authState != .signedIn {
showLoginSheet = true
} else {
signOut()
}
The sign in/out
button should look like the following:
// Show `Sign out` iff user is not anonymous,
// otherwise show `Sign-in` to present LoginView() when tapped.
Button {
if authManager.authState != .signedIn {
showLoginSheet = true
} else {
signOut()
}
} label: {
Text(authManager.authState != .signedIn ? "Sign-in" :"Sign out")
.font(.body.bold())
.frame(width: 120, height: 45, alignment: .center)
.foregroundStyle(Color(.loginYellow))
.background(Color(.loginBlue))
.cornerRadius(10)
}
Lastly, add the following at the end of VStack:
.sheet(isPresented: $showLoginSheet) {
LoginView()
}
We don’t want to show the skip
button when the user is already anonymously authenticated. So, in LoginView
, wrap theskip
button with the following condition:
if authManager.authState == .signedOut {}
Now, if you run the code 📲 you will see the sign-in option visible to the user, and when you tap on Sign-in
it will present LoginView
, but without showing the skip button.
Link sign-in credentials to the anonymous account
When an anonymous user signs up to your app, you might want to allow them to continue their work with their new account — for example, you might want to make the items the user added to their shopping cart before they signed up available in their new account’s shopping cart.
~ Convert an anonymous account to a permanent account
Before we continue to any service provider authentication, I want to add generic methods in AuthManager
that will be used with any service provider (except Email of course).
There are two ways to authenticate a user
signIn(with:)
onauth
object, to authenticate user for the first time, or after the user signed out.link(with:)
to link an already authenticated user with new credentials.
You can allow users to sign in to your app using multiple authentication providers by linking auth provider credentials to an existing user account. Users are identifiable by the same Firebase user ID regardless of the authentication provider they used to sign in.
~ Link Multiple Auth Providers to an Account on Apple Platforms
Let’s create these three generic functions in AuthManager
:
authenticateUser(credentials:)
takesAuthCredential
, and checks if we have an authenticated user. If so, it will callauthLink(credentials:)
, otherwise, it will callauthSignIn(credentials:)
.authSignIn(credentials:)
takes anAuthCredential
, and authenticates a user using the given credentials.authLink(credentials:)
takes anAuthCredential
, and links the given credentials to the currently authenticated user.
We will keep editing these methods along the way through this tutorial.
// 1.
private func authenticateUser(credentials: AuthCredential) async throws -> AuthDataResult? {
if Auth.auth().currentUser != nil {
return try await authLink(credentials: credentials)
} else {
return try await authSignIn(credentials: credentials)
}
}
// 2.
private func authSignIn(credentials: AuthCredential) async throws -> AuthDataResult? {
do {
let result = try await Auth.auth().signIn(with: credentials)
updateState(user: result.user)
return result
}
catch {
print("FirebaseAuthError: signIn(with:) failed. \(error)")
throw error
}
}
// 3.
private func authLink(credentials: AuthCredential) async throws -> AuthDataResult? {
do {
guard let user = Auth.auth().currentUser else { return nil }
let result = try await user.link(with: credentials)
// TODO: Update user's displayName
updateState(user: result.user)
return result
}
catch {
print("FirebaseAuthError: link(with:) failed, \(error)")
throw error
}
}
One more step
When linking provider credentials to an existing user account, the user won’t be updated with provider’s
displayName
, and the reason for that is explained in the link below:
So to update user
’s displayName
, we will get the displayName
from providerData
and update user’s profile after linking.
Create the following updateDisplayName(for:)
function:
- Check if
currentUser
containsdisplayName
, and that it is not empty, we don’t want to overwrite it if it already containsdisplayName
. - Get
displayName
fromproviderData
. - Call
createProfileChangeRequest()
onuser
object, to createsUserProfileChangeRequest
object which used to change user’s profile data, then setdisplayName
. - Finally, commit changes.
private func updateDisplayName(for user: User) async {
// 1.
if let currentDisplayName = Auth.auth().currentUser?.displayName, !currentDisplayName.isEmpty {
// current user is non-empty, don't overwrite it
} else {
// 2.
let displayName = user.providerData.first?.displayName
// 3.
let changeRequest = user.createProfileChangeRequest()
changeRequest.displayName = displayName
do {
// 4.
try await changeRequest.commitChanges()
}
catch {
print("FirebaseAuthError: Failed to update the user's displayName. \(error.localizedDescription)")
}
}
}
Replace // TODO: Update user’s displayName
in authLink(credentials:)
with await updateDisplayName(for: result.user)
.
Next Steps
Now that Firebase Authentication is set up, and users can authenticate anonymously, let’s add Google Sign-In next.
References
“ Everyone has something to learn. Everyone has something to teach.” ~ Paul Hudson