SwiftUI Firebase Authentication Tutorial: Complete iOS Login System

Building secure iOS authentication for your SwiftUI apps just got easier. This SwiftUI Firebase tutorial shows you how to implement a complete login system using Firebase Auth—Google’s robust authentication platform that’s been the gold standard for mobile app development.

Whether you’re building your first iOS app authentication system or looking to learn more about Firebase, this guide provides everything you need. You’ll learn to create secure sign-in, registration, and session management with clean, production-ready SwiftUI code.

Unlike basic authentication guides, we’ll build a complete system with proper error handling, input validation, and real-world best practices that you can deploy immediately.

What You’ll Learn

By the end of this tutorial, you’ll have a fully functional authentication system that includes:

  • User registration with email verification
  • Secure sign-in functionality
  • Password reset capabilities
  • Session management
  • Error handling and user feedback
  • Clean, reusable SwiftUI components

Prerequisites

Before we dive in, make sure you have:

  • Xcode 14+ installed
  • Basic knowledge of SwiftUI
  • A Google/Firebase account (free tier available)
  • iOS 15+ as your deployment target

Setting Up Your Firebase Project

First, let’s set up the backend infrastructure:

1. Create a Firebase Project

  1. Visit firebase.google.com and sign in with your Google account
  2. Click “Create a project” and follow the setup wizard
  3. Enter your project name and configure analytics (optional)
  4. Wait for your project to be created (usually takes 1-2 minutes)
  5. In the Firebase console, click “Add app” and select iOS
  6. Register your app with your bundle identifier
  7. Download the GoogleService-Info.plist file
  8. Place the .plist file inside your project like the picture below

2. Configure Authentication Settings

To simplify this tutorial we are going to disable email verification – when you go to production, it’s good practice to keep this enabled and set it up correctly. But this tutorial is focused on implementing Firebase inside an iOS application:

  1. Go to Authentication > Get Started > Sign-in method
  2. Click on Email/Password
  3. Enable Email/Password authentication
  4. Disable “Email link (passwordless sign-in)” for now
  5. Save your changes

iOS Project Setup with Firebase

Before we can do anything we need to create a new App, so open Xcode and create a new iOS project with SwiftUI interface.

Implement Firebase

Now it’s time to add Firebase to our project. Firebase provides excellent iOS SDKs that we’ll integrate using Swift Package Manager.

In Xcode, go to File → Add Package Dependencies and add:

https://github.com/firebase/firebase-ios-sdk

Select these packages for your project:

  • FirebaseAuth
  • FirebaseCore

Then add the GoogleService-Info.plist file you downloaded earlier to your Xcode project.

Setting Up Firebase in Your SwiftUI App

When building SwiftUI apps that need Firebase services, you’ll need to initialize Firebase before your app starts running. We’ll do that by creating a AppDelegate class. Here’s the standard way to do it using an App Delegate:

import UIKit
import FirebaseCore

class AppDelegate: NSObject, UIApplicationDelegate {

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

What’s happening here:

  • UIKit and FirebaseCore imports – UIKit gives us access to the app delegate protocol, while FirebaseCore contains the initialization methods
  • AppDelegate class creation – We create a custom app delegate that inherits from NSObject and conforms to UIApplicationDelegate
  • App launch method – The application(_:didFinishLaunchingWithOptions:) method runs automatically when your app starts
  • Firebase initialization – FirebaseApp.configure() reads your Firebase configuration file and sets up the connection
  • Success return – Returning true tells iOS that the app launched successfully

This App Delegate must be connected to your main SwiftUI App struct using @UIApplicationDelegateAdaptor to ensure Firebase initializes before your views load.

Setting Up the Main App Structure for Firebase Authentication

The main App struct is where everything comes together – it connects your App Delegate for Firebase initialization and launches your app’s main interface. Here’s how to structure your SwiftUI app entry point:

import SwiftUI

@main
struct FirebaseAuthTutorialApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

What’s happening here:

  • App entry point – @main attribute marks this as the application’s main entry point where execution begins
  • App protocol conformance – The struct conforms to the App protocol, which defines the structure of your SwiftUI app
  • Delegate adapter – @UIApplicationDelegateAdaptor connects your custom AppDelegate to the SwiftUI app lifecycle
  • Firebase initialization – The AppDelegate reference ensures Firebase.configure() runs before your views load
  • Scene definition – WindowGroup creates the main window that contains your app’s user interface
  • Root view setup – ContentView() becomes the first view users see when the app launches
  • SwiftUI lifecycle – This structure uses SwiftUI’s modern app lifecycle instead of the traditional UIKit approach
  • Automatic management – SwiftUI handles window management, scene transitions, and app state changes
  • Clean architecture – Separates app initialization (AppDelegate) from UI structure (App body)

This App struct serves as the foundation that ties together Firebase initialization with your SwiftUI authentication interface, ensuring everything loads in the correct order.

Creating a Firebase Authentication Manager for SwiftUI

Managing user authentication in SwiftUI apps requires a centralized system that can track login status and communicate changes to your views. Here’s a complete AuthManager class that handles all the essential authentication functions:

import Foundation
import FirebaseAuth

class AuthManager: ObservableObject {
    @Published var isSignedIn: Bool = false
    @Published var user: User?
    
    init() {
        checkAuthStatus()
    }
    
    func checkAuthStatus() {
        if Auth.auth().currentUser != nil {
            isSignedIn = true
            user = Auth.auth().currentUser
        }else {
            isSignedIn = false
            user = nil
        }
    }
    
    @MainActor
    func createUser(email: String, password: String) async throws {
        do {
            let result = try await Auth.auth().createUser(withEmail: email, password: password)
            self.isSignedIn = true
            self.user = result.user
        } catch {
            self.isSignedIn = false
            throw error
        }
    }
    
    @MainActor
    func signIn(email: String, password: String) async throws {
        do {
            let result = try await Auth.auth().signIn(withEmail: email, password: password)
            self.isSignedIn = true
            self.user = result.user
        } catch {
            self.isSignedIn = false
            throw error
        }
    }
    
    func signOut() throws {
        do {
            try Auth.auth().signOut()
            self.isSignedIn = false
            self.user = nil
        } catch {
            throw error
        }
    }
    
    func forgotPassword(email: String) throws {
        Auth.auth().sendPasswordReset(withEmail: email)
    }
}

What’s happening here:

  • ObservableObject setup – The class publishes authentication state changes to update SwiftUI views automatically
  • State tracking – isSignedIn and user properties maintain current authentication status
  • Automatic status check – Constructor calls checkAuthStatus() to detect existing user sessions
  • User registration – createUser() creates new Firebase accounts and updates local state on success
  • Sign in functionality – signIn() authenticates users and stores their information locally
  • Sign out capability – signOut() logs users out and clears all stored user data
  • Password reset feature – forgotPassword() sends password reset emails to users who forgot their credentials
  • Error propagation – All methods properly handle and re-throw Firebase errors for your app to manage
  • Main thread safety – Authentication methods use @MainActor to ensure UI updates happen on the main thread
  • Complete auth flow – Covers registration, login, logout, and password recovery for a full authentication system

This manager provides everything needed for a complete email/password authentication system in your SwiftUI app.

Building a Firebase Registration Screen in SwiftUI

Creating a user registration form requires input validation, loading states, and error handling. Here’s a complete SwiftUI registration view that integrates with our Firebase AuthManager:

import SwiftUI

struct RegisterView: View {
    @EnvironmentObject var authManager: AuthManager
    
    @State private var email: String = ""
    @State private var password: String = ""
    @State private var confirmPassword: String = ""
    @State private var isLoading = false
    @State private var showError = false
    @State private var errorMessage: String = ""
    
    var body: some View {
        VStack(spacing: 40) {
            Text("Register to Firebase")
                .font(.title)
                .bold()
            
            VStack(alignment: .leading, spacing: 20) {
                VStack(alignment: .leading) {
                    Text("Enter email below:")
                        .font(.subheadline)
                        .foregroundColor(.secondary)
                    TextField("Enter email", text: $email)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                }
                
                VStack(alignment: .leading) {
                    Text("Enter password below:")
                        .font(.subheadline)
                        .foregroundColor(.secondary)
                    SecureField("Enter password", text: $password)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                    SecureField("Re-enter password", text: $confirmPassword)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                }
                
            }
            Button(action: {
                Task {
                    do {
                        isLoading = true
                        try await authManager.createUser(email: email, password: password)
                        isLoading = false
                    }catch {
                        isLoading = false
                        errorMessage = error.localizedDescription
                        print("Registration failed: \(error.localizedDescription)")
                        showError = true
                    }
                }
            }) {
                HStack {
                    if isLoading {
                        ProgressView()
                            .progressViewStyle(CircularProgressViewStyle(tint: .white))
                            .scaleEffect(0.8)
                    } else {
                        Text("Signup using Firebase")
                    }
                }
                .font(.headline)
                .foregroundColor(.white)
                .padding()
                .background(Color.blue)
                .cornerRadius(8)
            }
        }
        .alert("Registration failed!", isPresented: $showError) {
            Button("OK") {
                showError = false
            }
        } message: {
            Text("\(errorMessage)")
        }
        .padding()
    }
}

#Preview {
    RegisterView()
}

What’s happening here:

  • Environment object connection – @EnvironmentObject connects the view to the shared AuthManager instance
  • State management – @State properties track user input, loading status, and error states
  • Form structure – VStack layout creates a clean registration form with proper spacing
  • Input fields – TextField for email and SecureField for password entries with rounded border styling
  • Password confirmation – Second SecureField allows users to verify their password input
  • Async registration – Button action uses Task to handle the async createUser() method
  • Loading indicator – ProgressView appears during registration process, replacing button text
  • Error handling – Catches registration failures and displays user-friendly error messages
  • Alert system – Shows error alerts with specific failure messages from Firebase
  • Visual feedback – Button changes appearance during loading and shows appropriate content
  • Clean UI design – Consistent styling with proper typography, colors, and spacing throughout

This registration view provides a complete user experience with proper validation, feedback, and error handling for Firebase authentication.

Creating a Complete Firebase Sign-In Screen with Navigation

This SwiftUI view provides a full authentication interface with sign-in functionality and navigation to registration and password reset screens. Here’s the complete implementation:

import SwiftUI

struct SignInView: View {
    @EnvironmentObject var authManager: AuthManager
    @State private var email: String = ""
    @State private var password: String = ""
    @State private var showError: Bool = false
    @State private var errorMessage: String = ""
    
    var body: some View {
        NavigationStack {
            
            
            VStack(spacing: 40) {
                Text("Firebase AUTH tutorial")
                    .font(.title)
                    .bold()
                
                
                VStack(alignment: .leading, spacing: 20) {
                    VStack(alignment: .leading, spacing: 3) {
                        Text("Enter your email below:")
                            .font(.subheadline)
                            .foregroundColor(.secondary)
                        TextField("Enter your email", text: $email)
                            .textFieldStyle(RoundedBorderTextFieldStyle())
                    }
                    
                    VStack(alignment: .leading, spacing: 3) {
                        Text("Enter your password below:")
                            .font(.subheadline)
                            .foregroundColor(.secondary)
                        SecureField("Enter your password", text: $password)
                            .textFieldStyle(RoundedBorderTextFieldStyle())
                    }
                    
                }
                
                VStackLayout(spacing: 10) {
                    Button(action: {
                        Task {
                            do {
                                try await authManager.signIn(email: email, password: password)
                            } catch {
                                print("Login failed: \(error.localizedDescription)")
                                errorMessage = error.localizedDescription
                                showError = true
                            }
                        }
                    }) {
                        Text("Login using Firebase")
                            .font(.headline)
                            .foregroundColor(.white)
                            .padding()
                            .background(Color.blue)
                            .cornerRadius(8)
                    }
                    
                    NavigationLink(destination:
                                    RegisterView()
                        .environmentObject(authManager)
                    ) {
                        Text("Register")
                            .font(.subheadline)
                    }
                    
                    NavigationLink(destination:
                                    ForgotPasswordView()
                        .environmentObject(authManager)
                    ) {
                        Text("Forgot password")
                            .font(.subheadline)
                    }
                    
                }
            }
            .alert("Login failed!", isPresented: $showError) {
                Button("OK") {
                    showError = false
                }
            } message: {
                Text("\(errorMessage)")
            }
            .padding()
        }
    }
}

#Preview {
    SignInView()
}

What’s happening here:

  • Navigation wrapper – NavigationStack enables navigation between sign-in, registration, and password reset screens
  • AuthManager integration – @EnvironmentObject connects to the shared authentication manager
  • Input state management – @State properties track email, password, and error states
  • Clean form layout – VStack structure creates organized input sections with proper spacing
  • Input fields – TextField for email and SecureField for password with rounded border styling
  • Async sign-in – Button uses Task to handle the async signIn() method from AuthManager
  • Error handling – Catches sign-in failures and displays specific error messages to users
  • Navigation links – NavigationLink components provide seamless transitions to other authentication screens
  • Environment object passing – Each destination view receives the AuthManager through .environmentObject()
  • User-friendly navigation – Register and forgot password links offer alternative authentication paths
  • Alert system – Error alerts show Firebase authentication failures with helpful messages
  • Consistent styling – Uniform typography, colors, and spacing throughout the interface

This sign-in view serves as the main authentication hub, connecting users to all essential authentication functions in your Firebase-powered SwiftUI app.

Forgot password screen for Firebase authentication

When users forget their passwords, they need a simple way to reset them. Here’s a complete SwiftUI view that handles password reset requests through Firebase:

import SwiftUI

struct ForgotPasswordView: View {
    @EnvironmentObject var authManager: AuthManager
    @State private var email: String = ""
    @State private var showSuccess = false
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        NavigationStack {
            VStack(spacing: 40) {
                Text("Forgot Password")
                    .font(.title)
                    .bold()
                
                VStack(alignment: .leading, spacing: 20) {
                    VStack(alignment: .leading, spacing: 3) {
                        Text("Enter your email below:")
                            .font(.subheadline)
                            .foregroundColor(.secondary)
                        TextField("Enter your email", text: $email)
                            .textFieldStyle(RoundedBorderTextFieldStyle())
                            .keyboardType(.emailAddress)
                            .autocapitalization(.none)
                    }
                }
                
                VStackLayout(spacing: 10) {
                    Button(action: {
                        Task {
                            authManager.forgotPassword(email: email)
                            showSuccess = true
                        }
                    }) {
                        HStack {
                            Text("Send Reset Link")
                        }
                        .font(.headline)
                        .foregroundColor(.white)
                        .padding()
                        .background(Color.blue)
                        .cornerRadius(8)
                    }
                    .disabled(email.isEmpty)
                    
                    Button("Back to Sign In") {
                        dismiss()
                    }
                    .font(.subheadline)
                }
            }
            .padding()
            .alert("Reset Link Sent!", isPresented: $showSuccess) {
                Button("OK") {
                    dismiss()
                }
            } message: {
                Text("We've sent a password reset link to \(email). Please check your inbox.")
            }
        }
    }
}

#Preview {
    ForgotPasswordView()
}

What’s happening here:

  • Environment integration – @EnvironmentObject connects to the shared AuthManager and @Environment(\.dismiss) handles view dismissal
  • Simple state management – @State properties track email input and success alert visibility
  • Clean interface design – VStack layout creates a focused, single-purpose screen with proper spacing
  • Email-optimized input – TextField configured with email keyboard type and disabled autocapitalization
  • Smart button states – Send button is disabled when email field is empty to prevent invalid requests
  • Password reset functionality – Calls authManager.forgotPassword() to send reset email through Firebase
  • Success feedback – Alert confirms when reset link has been sent with personalized message
  • Navigation handling – Back button and success alert both dismiss the view to return to sign-in
  • User experience flow – Task wrapper handles the password reset request smoothly
  • Accessibility features – Proper keyboard type and input formatting for better usability
  • Clean visual hierarchy – Consistent styling matches other authentication screens in the app

This password reset view provides a streamlined experience for users who need to recover their accounts, integrating seamlessly with your Firebase authentication system.

Creating the Main Content View with Authentication Flow

The ContentView acts as your app’s traffic controller, deciding whether to show the authentication screens or the main app content based on user login status. Here’s how to implement this crucial navigation logic:

import SwiftUI

struct ContentView: View {
    @StateObject private var authManager = AuthManager()

    var body: some View {
        if authManager.isSignedIn {
            HomeView()
                .environmentObject(authManager)
        }else {
            SignInView()
                .environmentObject(authManager)
        }
    }
}

#Preview {
    ContentView()
}

What’s happening here:

  • Creates AuthManager – @StateObject makes a single AuthManager for the whole app
  • Checks login status – Shows HomeView if signed in, SignInView if not
  • Shares AuthManager – .environmentObject() lets other views use the same AuthManager
  • Automatic updates – View changes when user signs in or out

This is your app’s main decision point – it decides what screen to show based on whether the user is logged in.

Basic Home Screen with Sign Out

This HomeView shows what authenticated users see, with a simple sign-out button:

import SwiftUI

struct HomeView: View {
    @EnvironmentObject var authManager: AuthManager

    var body: some View {
        Text("Hello, World!")
        
        Button {
            Task {
                 try authManager.signOut()
            }
        
        }label: {
            Text("Sign Out")
        }
    }
}

#Preview {
    HomeView()
}

What’s happening here:

  • Welcome text – Shows “Hello, World!” to logged-in users
  • Sign out button – Logs the user out when tapped
  • Auto navigation – App goes back to sign-in screen after sign out

Simple home screen that lets users sign out and return to the login flow.

Conclusion

You’ve created a professional-grade authentication system that would typically take weeks to build from scratch. Your SwiftUI app now includes secure registration, reliable sign-in, password recovery, and persistent sessions—all leveraging Firebase’s battle-tested security infrastructure.

Now you just need to build the rest of your application 😉

Get the Complete Code

Download the full source code on GitHub →

Frequently Asked Questions – SwiftUI Firebase Authentication

Auth domain not authorized” error – what does this mean?

In Firebase Console, go to Authentication > Settings > Authorized domains and add your development/production domains.

Can I customize the password reset email template?

Yes, in Firebase Console go to Authentication > Templates to customize email templates, including branding and content.

Do I need a paid Firebase account to use authentication?

No, Firebase Authentication is free for up to 10,000 monthly active users. The free tier includes email/password, phone, and social logins.

Why am I getting “GoogleService-Info.plist not found” error?

Ensure the .plist file is added to your Xcode project (not just the folder) and is included in your app target. Check that the bundle identifier matches your Firebase project.

How do I handle authentication state across app launches?

Firebase automatically maintains authentication state. The checkAuthStatus() method in AuthManager detects existing sessions when the app starts.

How do I implement proper password validation?

Add validation in your registration view: minimum 8 characters, mix of letters/numbers/symbols. Firebase also provides built-in password strength requirements.

Can I add social login (Google, Apple) to this setup?

Yes, you can extend the AuthManager to include social providers. You’ll need to add the respective Firebase provider dependencies and configure them in your Firebase console.

Scroll to Top