User Authentication with SwiftUI and Firebase

Share on facebook
Share on twitter
Share on pinterest

In this tutorial, you will learn how to perform user authentication using Firebase and SwiftUI. You will learn how to easily create user accounts for your app. We’ll also look at how to let users log in and out of their accounts.

The app we create in this tutorial will end up looking like this:

Let’s get right into it!

The Starter Project 🛫

I have already prepared a starter project, which you can download here:

The starter project contains three different views we will work with. A SignUpView, where the user can create an account with his email and a personal password. A SignInView where the user can log in with his credentials. And finally, a HomeView represents the actual content of our app and which we will show as soon as the user has successfully registered or logged in.

Note that the starter project also includes a ViewRouter and a MotherView that allow us to navigate between different views. If you want to know exactly how this works, feel free to check out this tutorial.

Preparing our App for Firebase 🔥

To authenticate users we use Google’s Firebase service. Firebase is a great and simple way to add backend functionality like databases, analytics and even user authentication to SwiftUI apps.

Firebase is free for most features up to a certain level. For example, the free plan allows you to perform 10k user authentications per month. The only thing you need is a Google account.

Let’s register our app for Firebase services. To do this, open the Firebase Dashboard and click “Get Started” and then “Add Project”. 

Name your project as you like and then click “Continue”. If you want, you can also enable Google Analytics, but we don’t need it for our purposes. After you have created the Firebase project, you should see something like this:

To add Firebase to the Xcode project, click on the “iOS+” button in the Firebase Console and follow the instructions.

Copy and paste the bundle ID of your Xcode project (if you don’t have one yet, create one, for example “com.YOURNAME.AuthenticationDemoApp”) and click on “Register App”.  

Then download the generated “GoogleService-Info.plist” file and add it to the Xcode project (simply drag and drop it into the Project navigator). Make sure that you select “Copy items if needed”.

Now we just need to add the Firebase dependencies to our Xcode project. Open the Xcode menubar and select “File” – “Add Packages”. Then enter the URL “https://github.com/firebase/firebase-ios-sdk” into the search field and click on “Add Packages”.

After a few moments, we will be asked which Firebase components we want to install. We need the FirebaseAuth framework to perform user authentication in SwiftUI apps. Therefore, select “FirebaseAuth” and click on “Add Package”.

At this point, we need to initialize Firebase once we run the app. To do this, open the AuthenticationStarter.swift file and add the Firebase library and the following initialization code to it.

import SwiftUI
import Firebase

@main
struct AuthenticationInSwiftUIFinishedApp: App {
    
    @StateObject var viewRouter = ViewRouter()
    
    init() {
        FirebaseApp.configure()
    }
    
    var body: some Scene {
        WindowGroup {
            MotherView().environmentObject(viewRouter)
        }
    }
}

Also add the Firebase library to the SignUpView, SignInView and HomeView.

import SwiftUI
import Firebase

struct SignUpView: View {
    
    //...
    
    var body: some View {
        //...
    }
    
}
import SwiftUI
import Firebase

struct SignInView: View {
    
    //...
    
    var body: some View {
        //...
    }
    
}
import SwiftUI
import Firebase

struct HomeView: View {
    
    //...
    
    var body: some View {
        //...
    }
    
}

In the Firebase Console, we need to enable the authentication feature. To do this, we click on “Authentication” under the “Build” section and then on “Get started”.

We select “Email/Password” as the authentication method, but do not enable “Email link”.

That’s it! We have now set up everything to do user authentication using SwiftUI and Firebase.

Signing Up Users with Email and Password 📧

Let’s start with the SignUpView. We want to know if and when the authentication process is performed. For this, we add a corresponding State property to our SignUpView to which we initially assign false.

import SwiftUI
import Firebase

struct SignUpView: View {
    
    //...
    
    @State var signUpProcessing = false
    
    var body: some View {
        //...
    }
}

Next, we add a function that creates a new user account in Firebase using a given username and password. When this function is called we set the signUpProcessing State to true.

import SwiftUI
import Firebase

struct SignUpView: View {
    
    //...
    
    @State var signUpProcessing = false
    
    var body: some View {
        //...
    }

    func signUpUser(userEmail: String, userPassword: String) {
        
        signUpProcessing = true

    }
}

To create an account in Firebase we use the createUser method of the FirebaseAuth dependency we installed earlier. 

import SwiftUI
import Firebase

struct SignUpView: View {
    
    //...
    
    @State var signUpProcessing = false
    
    var body: some View {
        //...
    }

    func signUpUser(userEmail: String, userPassword: String) {
        
        signUpProcessing = true

        Auth.auth().createUser(withEmail: userEmail, password: userPassword) { authResult, error in
            
        }

    }
}

The value authResult tells us if the signup process returned a result. The error value in the createUser closure throws us any errors, for example if an account already exists under the given email.

Next, we use a guard statement to make sure that no such errors occurred. Otherwise, we terminate the authentication process.

func signUpUser(userEmail: String, userPassword: String) {
    
    signUpProcessing = true
    
    Auth.auth().createUser(withEmail: userEmail, password: userPassword) { authResult, error in
        guard error == nil else {
            signUpProcessing = false
            return
        }
        
    }
}

Next, let’s see if we received a result from Firebase. If this is the case, we end the authentication process and navigate to the .homePage using the viewRouter.

func signUpUser(userEmail: String, userPassword: String) {
    
    signUpProcessing = true
    
    Auth.auth().createUser(withEmail: userEmail, password: userPassword) { authResult, error in
        guard error == nil else {
            signUpProcessing = false
            return
        }
        
        switch authResult {
        case .none:
            print("Could not create account.")
            signUpProcessing = false
        case .some(_):
            print("User created")
            signUpProcessing = false
            viewRouter.currentPage = .homePage
        }
    }
}

We now call the signUpUser function from the “Sign Up” Button using the values from the username and password TextField.

Button(action: {
    signUpUser(userEmail: email, userPassword: password)
}) {
    //...
}

To prevent the user from creating an account without providing a password and email, we disable the “Sign Up” Button if the TextFields are empty. Also, we disable it if the String of the password TextField and that of the confirmationPassword TextField are not identical. 

Button(action: {
    signUpUser(userEmail: email, userPassword: password)
}) {
    //...
}
    .disabled(!signUpProcessing && !email.isEmpty && !password.isEmpty && !passwordConfirmation.isEmpty && password == passwordConfirmation ? false : true)

That’s it! We can now check if this works. Let’s run our app, navigate to the sign up page and use any email and password (with at least six digits). 

When we click on the Sign Up Button, a user account will be created for us on Firebase and we will be navigated to the HomeView.

We can check if this worked on the server-side by opening the console of our Firebase project and going to the Authentication page. Under the “User” tab we can now see the user account we just created!

Next, we want to improve the user experience by displaying a spinning activity indicator while the registration process is running. To do this, we simply add a ProgressView to our VStack when signUpProcessing is false.

var body: some View {
    VStack(spacing: 15) {
        //...
        Button(action: {
            signUpUser(userEmail: email, userPassword: password)
        }) {
            //...
        }
        if signUpProcessing {
            ProgressView()
        }
        Spacer()
        //...
    }
        .padding()
}

We also want to tell the user if any errors have occurred. To do this, we create a new State that holds the error message as a String.

@State var signUpErrorMessage = ""

Inside the guard statement of our signUpUser function, we now use the error property to assign the error description to the State property.

guard error == nil else {
    signUpErrorMessage = error!.localizedDescription
    signUpProcessing = false
    return
}

Finally, we add a Text view with the error message to our VStack, on the condition that one exists.

var body: some View {
    VStack(spacing: 15) {
        //...
        Button(action: {
            signUpUser(userEmail: email, userPassword: password)
        }) {
            //...
        }
        if signUpProcessing {
            ProgressView()
        }
        if !signUpErrorMessage.isEmpty {
            Text("Failed creating account: \(signUpErrorMessage)")
                .foregroundColor(.red)
        }
        Spacer()
        //...
    }
        .padding()
}

Now let’s run our app one more time and use for example an already existing email or a password that is too insecure.

Signing In Users 👤

So far, we know how to successfully create a user account using SwiftUI and Firebase. In the next step, let’s look at how we can enable the user to log in using their credentials.

To do this, we open our SignUpView. Again, we want to know if and when the authentication process is performed. For this, we add a corresponding State property to our SignInView to which we initially assign false. Again, we also add a State holding the potential error message.

@State var signInProcessing = false
@State var signInErrorMessage = ""

Next, we add a function that signs the user in using the username and password. When this is called we set the signUpProcessing State to true.

func signInUser(userEmail: String, userPassword: String) {
    
    signInProcessing = true

}

To sign in the user we use the FirebaseAuth signIn method.

func signInUser(userEmail: String, userPassword: String) {
    
    signInProcessing = true
    
    Auth.auth().signIn(withEmail: email, password: password) { authResult, error in
        
        
    }

}

This function works very similar to the createUser method and also provides us with an authResult and error value. The authResult value tells us if the sign-up process produced a result. The error value in the createUser closure throws us any errors, for example that the user credentials are incorrect.

So again we use a guard statement to make sure that no such errors occurred. Otherwise, we terminate the authentication process and assign the corresponding error description to the signUpErrorMessage State.

func signInUser(userEmail: String, userPassword: String) {
    
    signInProcessing = true
    
    Auth.auth().signIn(withEmail: email, password: password) { authResult, error in
        
        guard error == nil else {
            signInProcessing = false
            signInErrorMessage = error!.localizedDescription
            return
        }
        
    }

}

Next, we check if we have received a result from Firebase. If so, we terminate the authentication process and navigate to the .homePage using the viewRouter. If it fails we set signInProcessing to false.

func signInUser(userEmail: String, userPassword: String) {
    
    signInProcessing = true
    
    Auth.auth().signIn(withEmail: email, password: password) { authResult, error in
        
        guard error == nil else {
            signInProcessing = false
            signInErrorMessage = error!.localizedDescription
            return
        }
        switch authResult {
        case .none:
            print("Could not sign in user.")
            signInProcessing = false
        case .some(_):
            print("User signed in")
            signInProcessing = false
            withAnimation {
                viewRouter.currentPage = .homePage
            }
        }
        
    }

}

We use the “Log In” Button to call this function. Again we want to disable the “Log In” button if the TextFields are empty. 

Button(action: {
    signInUser(userEmail: email, userPassword: password)
}) {
    Text("Log In")
        //...
}
    .disabled(!signInProcessing && !email.isEmpty && !password.isEmpty ? false : true)

As we did with our SignUp view, let’s add a spinning activity indicator to the SignInView while the sign in process is ongoing. Additionally, we show a Text view if the sign in process throws any error.

//...
Button(action: {
    signInUser(userEmail: email, userPassword: password)
}) {
    //...
}
    .disabled(!signInProcessing && !email.isEmpty && !password.isEmpty ? false : true)
if signInProcessing {
    ProgressView()
}
if !signInErrorMessage.isEmpty {
    Text("Failed creating account: \(signInErrorMessage)")
        .foregroundColor(.red)
}
//...

Let’s check if this works by running our app and trying to sign in using the credentials of the account we created earlier.

Signing Out Users 🚪

So the user is now tagged as logged in with his account in Firebase. We also want to provide the user with the ability to log out from the HomeView. To do this, we add the following function to the HomeView.

func signOutUser() {
    let firebaseAuth = Auth.auth()
    do {
      try firebaseAuth.signOut()
    } catch let signOutError as NSError {
      print("Error signing out: %@", signOutError)
    }
    withAnimation {
        viewRouter.currentPage = .signInPage
    }
}

This uses the firebaseAuth.signOut() method to log the user out of their Firebase account again. Once this task is completed we navigate back to the SignInView using the viewRouter.

We add a Button to our HomeView in the navigation bar using the .toolbar modifier and a ToolbarItem view.

NavigationView {
    Text("HomeView")
        .navigationTitle("V24")
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                Button("Sign Out") {
                    
                }
            }
        }
}

With this Button, we now call the signOutUser method.

NavigationView {
    Text("HomeView")
        .navigationTitle("V24")
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                Button("Sign Out") {
                    signOutUser()
                }
            }
        }
}

While the sign-out process is still ongoing, we want to display a spinning activity indicator again instead of the “Sign Out” button. To do this, we add a “signOutProcessing” State to our HomeView. If this is true, we show a simple ProgressView instead of the ToolbarItem.

struct HomeView: View {
    
    //...
    
    @State var signOutProcessing = false
    
    var body: some View {
        NavigationView {
            Text("HomeView")
                .navigationTitle("V24")
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        if signOutProcessing {
                            ProgressView()
                        } else {
                            Button("Sign Out") {
                                signOutUser()
                            }
                        }
                    }
                }
        }
    }
    
    //...
}

Finally, we just have to set the signOutProcessing State to true when we call the signOutUser function. However, when the firebaseAuth.signOut method fails, we set signOutProcesssing to false again.

func signOutUser() {
    signOutProcessing = true
    let firebaseAuth = Auth.auth()
    do {
      try firebaseAuth.signOut()
    } catch let signOutError as NSError {
      print("Error signing out: %@", signOutError)
        signOutProcessing = false
    }
    withAnimation {
        viewRouter.currentPage = .signInPage
    }
}

Now when we run our app we can also manually log out of the app!

Where to go from here 🎉

I have uploaded the finished project to GitHub. Please note that I removed the GoogleService-Info.plist file. So to run the project you have to add your own first.

That’s it! You now know how to authenticate users in SwiftUI using Firebase. You have learned how to create user accounts, log in and log out users using email and password.

Other user authentication methods (like SMS, Sign in with Google, Sign in with Apple, etc.) can be easily added to your SwiftUI apps. Have a look at the official Firebase documentation. If you need specific help, feel free to let me know in the comments!

Leave a Reply

Your email address will not be published. Required fields are marked *

small_c_popup.png

Covid-19 Forces you into quarantine?

Start Mastering swiftUI Today save your 33% discount