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:
- Setting up Firebase Project (install Firebase SDK, set up in Firebase Console)
- Designing the UI of the app using SwiftUI (Login, Registration, Home screens)
- Registration with Firebase authentication
- Login with Firebase Auth
- Saving user details in Firestore
- Persistent login credentials
- 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.
- Head over to Firebase.com and create a new account or log in.
- Create a new project in Firebase Console
- Once a new project is created, click Authentication.
- 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:
- Head over to "Project Overview" and select "Add iOS App".
- This will ask you for your iOS app bundle ID. You can get this ID from the Xcode, select your Target > General > Bundle Identifier
- 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:
- In Xcode go to File > Swift Packages > Add Package Dependency…
- You will see a prompt appear, please add this link there: https://github.com/firebase/firebase-ios-sdk.git
- Select the version of Firebase you would like to use. Make sure it's the latest one.
- 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.
- MainView
- ContentView
- LoginView
- RegisterView
- 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:
- First we are checking if the
password
andconfirmedPassword
fields both match. If they don't we will show anAlert
and return from the function. - If all is good, using the Firebase SDK which we have imported, we will create a user using email and password.
- If user creation is successful, we shall get a response with user
UID
. - We will create a
User
object. The User object is basically a struct that contains three properties -id
,fullName
,email
. - 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. - Make sure you store user
UID
in theUsers
table. We will use thisUID
to fetch data when the user logs in. - Once the data is stored, we toggle
showHomeScreen
boolean which activates thefullScreenCover
modal and shows theHomeView
. TheHomeView
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.
- Using the email address and password provided by the user, we will sign in.
- The closure returns with an
error
andauthResult
. We check if the error is nil, if it is we call thefetchDocuments
function. - The
fetchDocuments
function gets theid
from the API response, and then using a simple Firebase query we are getting all the data from theUsers
collection. - We again create the
user
object with theid
,fullName
andemail
that we are getting from the Firestore. This is the same data which we stored when registering the user. - The we toggled the
showHomeScreen
boolean to true which invokes thefullScreenCover
modal and shows us theHomeView
. TheHomeView
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.