Authentication is a must feature for most of the mobile applications. In this blog post we are going to implement login and signup mechanisms to a SwiftUI application using Firebase Authentication.

Creating a SwiftUI Project

Open up Xcode and create a new SwiftUI project.

Firebase SDK Setup

First, we need to register our Firebase app in Firebase Console. Create a Firebase project if you don't have one and head to Project Overview to add an iOs app with your project's bundle id. Follow the instructions over there to finish setting up your app. When you are adding pods to your Podfile add the following pod:

pod 'Firebase/Auth'

After installing pods, reopen your Xcode project by using .xcworkspace file and add the initialization code below to your main AppDelegate class.

import UIKit
import Firebase // NEW

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

  var window: UIWindow?

  func application(_ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions:
      [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    FirebaseApp.configure() // NEW
    return true
  }
}

Firebase Auth Setup

We need to enable a Sign-in method from the Firebase Console. To do that, head to the Authentication > Sign-in method and enable Email/Password authentication.

Enabling
Enabling Email/Password authentication

Handling Auth State with SwiftUI

We need a way to manage Auth state through out the app. Depending on our state we are going to update the UI whether it is a Sign In / Sign Up page or Home page. Apart from that we would want to easily access user information regardless what view we are in. Remember that in SwiftUI, and declarative frameworks in general, UI is a function of state. So we are going to stick on that. State management in SwiftUI is a whole another topic and will be covered in another blog post but let's try to understand what we are going to use.

There are several ways for managing state in SwiftUI. We have the following properties:

@State:  Simple state, mostly used in a specific view and shouldn't be used outside that view. SwiftUI will handle the state change and update the UI with the recent change.
@ObservedObject: More complex state, can be shared across multiple views, You will decide on what will changes will trigger and update for the listening views.
@EnvironmentObject:
Application wide state, similar to ObservedObject but instead of passing it view to view, the data is shared across the views. Great for data that needs to be used a lot across the application.

It seems like @EnvironmentObject would be useful for our case since we want our views to know about the auth state. Let's continue with that.

Create a User class to store Firebase user information.

class User {
    var uid: String
    var email: String?
    
    init(uid: String, email: String?) {
        self.uid = uid
        self.email = email
    }
}

Create a new class called Auth. This class will use the ObservableObject protocol and any variable with @Published property will notify listening views on change. We are going to make use of addStateDidChangeListener method in Firebase Auth to listen for auth changes and will update the user object. If there is no user logged in it will be nil, if there is one it will be stored in user object. The user object will have a @Published property so listening views will be notified when there is a change.

import SwiftUI
import Firebase

class Auth: ObservableObject {
    
    @Published var user: User?
    var handle: AuthStateDidChangeListenerHandle?
    
    init() {
        handle = Auth.auth().addStateDidChangeListener { (auth, user) in
            if let user = user {
                self.user = User(
                    uid: user.uid,
                    email: user.email
                )
            } else {
                self.session = nil
            }
        }
    }
}

We also need to add the following functions to handle sign up and sign in.

func signUp(
    email: String,
    password: String,
    handler: @escaping AuthDataResultCallback
) {
    Auth.auth().createUser(withEmail: email, password: password, completion: handler)
}

func signIn(
    email: String,
    password: String,
    handler: @escaping AuthDataResultCallback
) {
    Auth.auth().signIn(withEmail: email, password: password, completion: handler)
}

We are done with the Auth class. Remember that we want to use this class as an EnvironmentObject to be able to access it from any view regardless where we are in the hierarchy. In your SceneDelegate class, create an instance of it(1) and set it as an environment object of contentView(2)

(1) Create an Auth instance

var auth = Auth()

(2) Set it as an environment object

window.rootViewController = UIHostingController(rootView:contentView.environmentObject(auth))

Your SceneDelegate class should look like this after making these changes:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?
    var auth = Auth() // 1. Create an Auth instance

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

        // Create the SwiftUI view that provides the window contents.
        let contentView = ContentView()

        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView:contentView.environmentObject(auth)) // 2. Set it as an environment object
            self.window = window
            window.makeKeyAndVisible()
        }
    }
... // rest of the class 

Great, we can now access this object across the entire app! In Part 2 we are going to build the UI.