How to Build A SwiftUI App With Firebase

In this article, we will create an iOS app which is integrated with Firebase backend for authentication. We will use SwiftUI to create the app's UI.

You can download the source code for this project from here.


The tutorial will be divided into the following sections:

  1. Setting up Firebase Project (install Firebase SDK, set up in Firebase Console)
  2. Designing the UI of the app using SwiftUI (Login, Registration, Home screens)
  3. Registration with Firebase authentication
  4. Login with Firebase Auth
  5. Saving user details in Firestore
  6. Persistent login credentials
  7. Logout

So, let's start building SwiftUI Firebase project.

1. Setting Up The Firebase Project

Head over to Firebase.com and create a new account. Once logged in, create a new project in the Firebase Console.

  1. Head over to Firebase.com and create a new account or log in.
  2. Create a new project in Firebase Console
  3. Once a new project is created, click Authentication.
  4. Select "Get Started" and then make sure you enable "Email/Passwords" under sign-in methods.

Next, we will add our iOS app with the Firebase console. Here is how it goes:

  1. Head over to "Project Overview" and select "Add iOS App".
  2. This will ask you for your iOS app bundle ID. You can get this ID from the Xcode, select your Target > General > Bundle Identifier
  3. Next, download the configuration file generated at the next step to your computer (GoogleService-Info.plist) we will simple drag and drop this .plist file in the root folder of our Xcode Project

Next we will need to install Firebase SDK. If you don't have Firebase already install then you have one of two ways. You can use CocoaPods or Swift Package Manager. I used Swift Package Manager to install SDK on my Xcode.

Here is how to do this:

  1. In Xcode go to File > Swift Packages > Add Package Dependency…
  2. You will see a prompt appear, please add this link there: https://github.com/firebase/firebase-ios-sdk.git
  3. Select the version of Firebase you would like to use. Make sure it's the latest one.
  4. Choose the Firebase products you would like to install.

So now we have the Firebase project all set up, our app connected to the Firebase console and Firebase SDK to communicate with the firebase backend from our iOS app. Firebase is a product running on top of Google Cloud and allows developers to build web and mobile apps without needing their own servers. This means you don't have to write your own backend code and saves a lot of time. Since Firebase is backed by Google infrastructure, so it is highly scalable.

Firebase can be used to authenticate users using multiple sign-in methods. You can use simple Email/Password set up, or you can authenticate using social media apps like Facebook, Instagram etc. You can store all the user credentials safely and can even store all other user data, files, push notifications, tokens, photos etc. All this information is given to mobile apps using Firebase's power SDK. The SDK allows developers to interact with the backend servers easily.

2. Building The UI of The iOS App in SwiftUI

The design of the app is really simple. We will have 4 screens in total. The first screen will show you "Login" or "Register" buttons. Tapping on any of the two buttons will be take you to the respective screens. Then we will have a Homescreen that will show up once the user signs up or logs in. However, in our SwiftUI app we will create 5 SwiftUI view files.

  1. MainView
  2. ContentView
  3. LoginView
  4. RegisterView
  5. HomeView

MainView is the view which the app will first launch to. This view will decide which screen to show up (ContentView or HomeView) after checking if the user is logged in or not. ContentView will contain two buttons: Login and RegisterView LoginView is a simple form that contains 2 textfields and a button. RegisterView contains a form with 4 textfields namely (FullName, Email, Password, Confirm Password) HomeView will contain a Welcome Message with users's name and UID.

So this is how our UI will be divided. The code for all the views is provided below:

  • ContentView.swift:
struct ContentView: View {
    @State private var presentLogInView: Bool = false
    @State private var presentSignUpView: Bool = false
    var body: some View {
        NavigationView {
            ZStack {
                VStack(spacing: 0) {
                    Image("firebase")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .padding()
                    Text("Firebase Authentication")
                        .font(.title)
                        .fontWeight(.bold)
                        .padding()

                    Spacer()
                    Button(action: {
                        print("Lets take you to log in screen")
                        self.presentLogInView = true
                    }, label: {
                        NavigationLink(
                            destination: LogInView(),
                            isActive: $presentLogInView,
                            label: {
                                Capsule()
                                    .frame(maxWidth: .infinity,maxHeight: 80, alignment: .center)
                                    .overlay(
                                        Text("Log In")
                                            .foregroundColor(.white)
                                            .font(.title)
                                    )
                                    .padding()
                            })
                    })

                    Button(action: {
                        print("lets take you to sign up screen")
                        self.presentSignUpView = true
                    }, label: {
                        NavigationLink(
                            destination: RegisterView(),
                            isActive: $presentSignUpView,
                            label: {
                                Capsule()
                                    .frame(maxWidth: .infinity,maxHeight: 80, alignment: .center)
                                    .foregroundColor(.orange)
                                    .overlay(
                                        Text("Sign Up")
                                            .foregroundColor(.white)
                                            .font(.title)
                                    )
                                    .padding()
                                    .padding(.top, -4)
                            })

                    })

                    Spacer()
                }.padding()
            }
        }
    }
}
  • LogInView.Swift:
struct LogInView: View {
    @State var username: String = ""
    @State var password: String = ""
    @State var showHomeScreen: Bool = false

    @State var user = User()
    var db = Firestore.firestore()

    var body: some View {
        VStack {
            Spacer()
            Text("Log In")
                .font(.title)
                .padding(.bottom, 44)
            TextField("Username or Email", text: $username)
                .padding(.horizontal, 32)
            Rectangle()
                .frame(maxWidth: .infinity, maxHeight: 1, alignment: .center)
                .padding(.horizontal, 32)
                .padding(.top, 2)
                .foregroundColor(.gray)

            SecureField("Password", text: $password)
                .padding(.horizontal, 32)
                .padding(.top, 16)
            Rectangle()
                .frame(maxWidth: .infinity, maxHeight: 1, alignment: .center)
                .padding(.horizontal, 32)
                .padding(.top, 2)
                .foregroundColor(.gray)

            Button(action: {
                self.handleLogInTapped()
            }) {
                Capsule()
                    .frame(maxWidth: .infinity,maxHeight: 60, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                    .overlay(
                        Text("Log In")
                            .foregroundColor(.white)
                            .font(.title)
                    )
                    .padding()
                    .padding(.top, 32)
                    .foregroundColor((username.isEmpty || password.isEmpty) ? .gray : .black)

            }.disabled((username.isEmpty || password.isEmpty))
            Spacer()
        }.padding()
        .fullScreenCover(isPresented: $showHomeScreen, content: {
                HomeView(user: self.$user)
          })
    }
}

    


The code above has a function handleLogInTapped - we will discuss this function later on this article.

  • RegisterView.Swift:

    struct RegisterView: View {
        @State var fullName: String = ""
        @State var email: String = ""
        @State var password: String = ""
        @State var confirmPassword: String = ""
        @State var showAlert: Bool = false
        @State var showHomeScreen: Bool = false
    
        var db = Firestore.firestore()
    
    
        @State var user = User()
    
        var body: some View {
    
            VStack {
                //Spacer()
                Text("Register Account")
                    .font(.title)
                    .padding(.bottom, 16)
    
    
                TextField("Full Name", text: $fullName)
                    .padding(.horizontal, 32)
                    .modifier(CustomBorder())
    
    
                TextField("Email", text: $email)
                    .padding(.horizontal, 32)
                    .padding(.top, 16)
                    .modifier(CustomBorder())
    
    
                SecureField("Password", text: $password)
                    .padding(.horizontal, 32)
                    .padding(.top, 16)
                    .modifier(CustomBorder())
    
                SecureField("Confirm Password", text: $confirmPassword)
                    .padding(.horizontal, 32)
                    .padding(.top, 16)
                    .modifier(CustomBorder())
    
                Button(action: {
                    self.handleRegisterTapped()
                }) {
                    Capsule()
                        .frame(maxWidth: .infinity,maxHeight: 40, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                        .overlay(
                            Text("Register")
                                .foregroundColor(.white)
                        )
                        .padding()
                        .padding(.top, 32)
    
    
                }
    
                Spacer()
    
    
            }.padding()
    
    
            .alert(isPresented: $showAlert) {
                Alert(title: Text("Password doesn't match"), message: Text("Please rewrite your password"), dismissButton: .default(Text("Okay!")))
            }
            .fullScreenCover(isPresented: $showHomeScreen, content: {
                    HomeView(user: self.$user)
              })
        }
      }

Custom Border Modifier is as follows:

struct CustomBorder: ViewModifier {
      func body(content: Content) -> some View {
          VStack {
          content

          Rectangle()
              .frame(maxWidth: .infinity, maxHeight: 1, alignment: .center)
              .padding(.horizontal, 32)
              .padding(.top, 2)
              .foregroundColor(.gray)
          }
      }
  }
  • HomeView.swift:
struct HomeView: View {
    @Binding var user: User
    @Environment(\.presentationMode) var presentationMode

    var body: some View {
        NavigationView {
            VStack {
                Text("Welcome, \(user.fullName ?? "")")
                Text("Your UID, \(Auth.auth().currentUser?.uid ?? "")")

                Button(action: {
                    do {
                        try  Auth.auth().signOut()
                        print(user)
                        print("Signed out")
                        self.presentationMode.wrappedValue.dismiss()
                    } catch let signOutError as NSError {
                        print("Error signing out: %@", signOutError)
                    }
                }, label: {
                    Text("Sign Out")
                })
            }.navigationTitle("Welcome, \(user.fullName ?? "")")
        }
    }
}


We will look at the code for MainView.swift when we discuss the credentials persistence.

Please make sure you are importing all the necessary Firebase modules:

import Firebase
import FirebaseFirestore

3. Registration with Firebase in SwiftUI

Now let's set up user registration with Firebase. Go to RegisterView file and create a function handleRegisterTapped and include the following code:

func handleRegisterTapped() {
    if self.password != confirmPassword {
        print("Error - Passwords don't match")
        self.showAlert = true
        return
    }


    Auth.auth().createUser(withEmail: self.email, password: self.password) { result, error in
        let id = result?.user.uid
        self.user = User(id: id, fullName: self.fullName, email: self.email)
        do {
            let _ = try db.collection("Users").addDocument(from: user)
            self.showHomeScreen = true
        }catch {
            print(error)
        }

    }
}


Let's discuss the code above:

  1. First we are checking if the password and confirmedPassword fields both match. If they don't we will show an Alert and return from the function.
  2. If all is good, using the Firebase SDK which we have imported, we will create a user using email and password.
  3. If user creation is successful, we shall get a response with user UID.
  4. We will create a User object. The User object is basically a struct that contains three properties - id, fullName, email.
  5. Then we we will store all this in the "Users" collection in Firestore since full name and other data is not stored in the authentication table.
  6. Make sure you store user UID in the Users table. We will use this UID to fetch data when the user logs in.
  7. Once the data is stored, we toggle showHomeScreen boolean which activates the fullScreenCover modal and shows the HomeView. The HomeView takes in this user object which we created earlier.

Go ahead and reload your app and test your registration. If registration is successful, you should see all the users in Authentication table in your Firebase account. Note: You only see their email address and UID in the Firebase console.

4. Login with Firebase in SwiftUI

Now that we have registered a user, we would like to start our Log In process. How to log in a user.

Lets start by heading over to the LogInView.swift file and adding the following functions:

func handleLogInTapped() {
    Auth.auth().signIn(withEmail: self.username, password: self.password) { authResult, error in
        if error == nil {
            print("Log In Successful",authResult)
            print("Welcome, \(Auth.auth().currentUser?.uid ?? "")")

            fetchDocuments(id: authResult?.user.uid ?? "")
        }

    }
}

func fetchDocuments(id: String) {
    db.collection("Users").whereField("id", isEqualTo: id)
      .getDocuments() { (querySnapshot, err) in
        if let err = err {
            print("Error getting documents: \(err)")
        } else {
            for document in querySnapshot!.documents {
                print(document.data())
                let docs = document.data()
                self.user = User(id: docs["id"] as? String, fullName: docs["fullName"] as? String, email: docs["email"] as? String)
            }

            self.showHomeScreen = true
        }
    }
}

When the user writes the email and password in the textfields and taps Login button, handleLogInTapped is invoked. Let's go through this code.

  1. Using the email address and password provided by the user, we will sign in.
  2. The closure returns with an error and authResult. We check if the error is nil, if it is we call the fetchDocuments function.
  3. The fetchDocuments function gets the id from the API response, and then using a simple Firebase query we are getting all the data from the Users collection.
  4. We again create the user object with the id, fullName and email that we are getting from the Firestore. This is the same data which we stored when registering the user.
  5. The we toggled the showHomeScreen boolean to true which invokes the fullScreenCover modal and shows us the HomeView. The HomeView again takes in the user object we just created.

And thats how you register a user, save all the user information that you may have asked during the registration process in the Firestore database and when logging in check if the log in is successful, and if it is get the user id and look for the related data in Users collection, create a user object and show the Home screen.

5. Persisting Credentials in Firebase Auth / Save Password


Next we will do Log In persistence. As you can see, if you reload the app, you have to log in again to get to the HomeView. This is not good user experience and we need to change it. Our app needs to remember if a user has logged in previously and to take it directly to the HomeView rather than ContentView. To do this, we will create a new view - MainView. The MainView will be the root view of our app. This view will decide which screen to show. We will also use SwiftUI property wrappers to help us with this logic. Let's begin:


This is the MainView.swift:

import SwiftUI
import Firebase
import FirebaseFirestore
import FirebaseFirestoreSwift

struct MainView: View {
    var db = Firestore.firestore()
    @ObservedObject var persistenceObserver: PersistenceObservation
    @State var showHomeScreen = true
    var body: some View {

        if persistenceObserver.uid == nil {
            ContentView()
        }else {
            ContentView()
                .fullScreenCover(isPresented: $showHomeScreen, content: {
                        HomeView(user: $persistenceObserver.user)
                  })
        }
    }
}


Before explaining this code, let's first create a class called PersistenceObservation which will conform to ObservableObject. Here is how:

import Foundation
import Combine
import Firebase
import FirebaseFirestore
import FirebaseFirestoreSwift

class PersistenceObservation: ObservableObject {
    @Published var uid: String?
    @Published var user = User()

    var db = Firestore.firestore()
    init() {
        self.uid = persistenceLogin()
        if uid != nil {
            fetchDocuments(id: uid!)
        }
    }

    func persistenceLogin() -> String? {
        guard let uid = Auth.auth().currentUser?.uid else {
            return nil
        }

        return uid
    }

    func fetchDocuments(id: String) {
        db.collection("Users").whereField("id", isEqualTo: id)
          .getDocuments() { (querySnapshot, err) in
            if let err = err {
                print("Error getting documents: \(err)")
            } else {
                for document in querySnapshot!.documents {
                    print(document.data())
                    let docs = document.data()
                    self.user = User(id: docs["id"] as? String, fullName: docs["fullName"] as? String, email: docs["email"] as? String)
                }
            }
        }
    }
}


In this class, we we have two Publishers, these are properties with @Published written in front of it. We have uid which is a string and a user of type User. In the initializer of this class, we are calling two functions, persistenceLogin and fetchDocuments. persistenceLogin function returns an optional string. Inside this function we are checking if a user is already logged in. If yes, it will give us a uid string else it will be nil. Now back in the init, we check if the uid is nil or not, if it is not nil, we call the fetchDocuments function and give it the user id which we just got. We again retrieve all the data from the Firestore, and assign it to the published User property.

So now, we are observing these two properties, the uid and the user. If any of these two things change, it will cause the view to re-render itself. Perfect! Now, let’s head back to MainView.swift.

struct MainView: View {
    var db = Firestore.firestore()
    @ObservedObject var persistenceObserver: PersistenceObservation
    @State var showHomeScreen = true
    var body: some View {

        if persistenceObserver.uid == nil {
            ContentView()
        }else {
            ContentView()
                .fullScreenCover(isPresented: $showHomeScreen, content: {
                        HomeView(user: $persistenceObserver.user)
                  })
        }
    }
}
 

Here we are created a ObservedObject of type PersistenceObservation and we are using a simple if condition on the persistenceObserver.uid. If it is nil, we will show the ContentView. If it is not nil, then we will show the HomeView with the user which we will get from persistenceObserver.user. The HomeView again is in fullScreenCover modal on top of the ContentView so that our sign out logic can still be implemented which takes us back to ContentView.

6. Logout with Firebase in SwiftUI

To sign out users, we are using this code in the HomeView.swift file. So when you press the sign out button this get’s invoked:

Button(action: {
     do {
       try  Auth.auth().signOut()
             self.presentationMode.wrappedValue.dismiss()
       } catch let signOutError as NSError {
             print("Error signing out: %@", signOutError)
        }
     }, label: {
             Text("Sign Out")
      })


So when you tap Sign Out, the modal view gets dismissed and we return to the ContentView screen.