How to Set Up Firebase Authentication in a SwiftUI iOS App

A practical guide to adding Firebase Authentication to a SwiftUI app, covering email/password sign-up and sign-in plus a complete Sign in with Apple flow.

Prerequisites and Enabling Sign-In Providers

Before writing any Swift, you need Firebase already added to your project, meaning the SDK is installed, GoogleService-Info.plist is a member of your app target, and FirebaseApp.configure() runs at launch. With that in place, add the FirebaseAuth product to your package if you have not already, through Xcode's Package Dependencies. Next, open the Firebase console, go to the Build section, and select Authentication. Click Get started, then open the Sign-in method tab. Here you enable the providers your app will offer. For this guide, enable Email/Password by toggling it on and saving. Then enable Apple as a provider, which is Firebase's integration point for Sign in with Apple. Apple's App Store Review Guidelines require that if you offer any third-party or social login, you must also offer Sign in with Apple, so wiring both is common. Enabling a provider in the console is what allows the client SDK to create and verify accounts of that type; skipping this step causes runtime errors when you try to sign in. Enable only the providers you actually intend to ship, since each one adds configuration and, for some, extra setup in the Apple or Google developer consoles.

Building an Auth Manager with an Observable Object

In SwiftUI, the cleanest way to expose authentication state is an ObservableObject that the rest of your UI observes. Create a class, for example AuthManager, conforming to ObservableObject. Import FirebaseAuth. Add a @Published property such as user of type User? (Firebase's user type) that reflects the current signed-in user, and set up an auth state listener in the initializer using Auth.auth().addStateDidChangeListener. That listener fires whenever the user signs in or out, so your UI can react automatically: show the login screen when user is nil and the main app when it is populated. Store the listener handle and remove it in deinit with removeStateDidChangeListener to avoid leaks. This pattern means you never manually track login state across views; you observe one source of truth. Inject the manager into your view hierarchy with @StateObject at the app's root and pass it down with @EnvironmentObject, so any view can trigger sign-in, sign-out, or read the current user without threading state through initializers. Because the listener also fires once at launch with any persisted session, your app can show the correct screen immediately without a flash of the login UI.

Email and Password Sign-Up and Sign-In

Email/password is the simplest provider to implement. To create an account, call Auth.auth().createUser(withEmail:password:) with the values from your text fields; the completion handler returns an AuthDataResult on success or an Error you should surface to the user. Firebase enforces a minimum password length of six characters and validates the email format, returning descriptive errors you can map to friendly messages, such as email already in use or weak password. To sign an existing user in, call Auth.auth().signIn(withEmail:password:) with the same shape of completion handler. Because you set up the state listener earlier, a successful call automatically updates your published user property and flips your UI to the signed-in state, so you generally do not need to do anything in the completion handler except handle errors. For a modern codebase, prefer the async/await versions of these APIs, awaiting try await Auth.auth().createUser(...) inside a Task and catching errors with do/catch, which reads far more cleanly than nested closures in SwiftUI actions. Keep validation on the client too, checking for empty fields and obvious formatting problems before you call Firebase, so you save a network round-trip and give users faster feedback.

Implementing Sign in with Apple

Sign in with Apple is more involved because it combines Apple's AuthenticationServices framework with Firebase. First, in Xcode, add the Sign in with Apple capability to your target under Signing & Capabilities, which requires your app to be part of a paid Apple Developer Program account. In your UI, present an ASAuthorizationAppleIDButton or use SwiftUI's SignInWithAppleButton. When the user taps it, you request an ASAuthorizationAppleIDRequest for the full name and email scopes and, importantly, attach a cryptographic nonce: generate a random nonce, keep the raw value, and send its SHA256 hash on the request. When Apple returns an ASAuthorizationAppleIDCredential, you extract the identity token, build a Firebase credential with OAuthProvider.appleCredential (or the credential initializer using provider ID apple.com, the ID token, and the raw nonce), and pass it to Auth.auth().signIn(with:). The nonce prevents replay attacks and is required by Firebase for the flow to verify correctly. Follow Firebase's official Apple sign-in guide closely, since the nonce and token handling are the parts developers most often get subtly wrong. Note that Apple accounts using the hide-my-email relay will give you a proxy address rather than the user's real email, which is fine for authentication but worth remembering if you plan to email users directly.

Reacting to Auth State in Your SwiftUI Views

With the auth manager and providers in place, your view layer becomes simple. At the root of your app, observe the AuthManager and branch on its user property: if it is nil, present your LoginView; otherwise present your main content. Because the state change listener updates that published property, sign-in and sign-out transitions animate automatically as SwiftUI re-renders. In your login view, wire your sign-in and sign-up buttons to methods on the manager, and display any thrown errors with an alert. For sign-out, call try Auth.auth().signOut(), which triggers the listener and returns the user to the login screen. It is good practice to also expose the current user's uid and email from the manager so downstream views can personalize content or scope Firestore queries to the signed-in user. This one-source-of-truth structure keeps authentication logic out of individual views and makes it easy to add more providers later without touching your navigation code. Wrapping the root switch in a transition or animation block gives you a polished fade between the authenticated and unauthenticated states with essentially no extra logic.

Handling Errors and Edge Cases

Real authentication code lives or dies on error handling. FirebaseAuth returns typed errors you can inspect via the AuthErrorCode enum, letting you distinguish an already-registered email from a wrong password or a network failure and show the right message. Always present errors to users in plain language rather than raw error strings. Consider the account-linking case: a user who signed up with email might later try Sign in with Apple using the same address, and you may want to link credentials rather than create a duplicate account, which FirebaseAuth supports through the link APIs. Handle the emailAlreadyInUse and credentialAlreadyInUse errors deliberately. Also account for the fact that Apple only returns the user's name and email on the very first sign-in, so capture and persist them then, because subsequent sign-ins will not include that data. Finally, test the signed-out state, token expiration, and offline launches, since your app should degrade gracefully when the network is unavailable or the session has expired. Building a small mapping from AuthErrorCode values to human-readable strings early pays off, because you will reuse it across every sign-in and sign-up path in the app.

Security, Verification, and Next Steps

Once sign-in works, tighten the surrounding security. Enable email verification for password accounts by calling sendEmailVerification on the user and gating sensitive features until isEmailVerified is true, which reduces spam and typo'd addresses. When you connect Firestore or Storage, write Security Rules that reference request.auth.uid so that users can only read and write their own data; authentication without matching rules gives you identity but not protection. Consider enabling Firebase App Check to reduce abuse from unauthorized clients. For production, review the Authentication settings in the console for authorized domains and any quota limits, and decide on your session persistence needs, since Firebase persists sessions by default so users stay logged in across launches. From here, common next steps are adding password reset via sendPasswordReset, offering additional providers like Google or phone auth, and using the signed-in uid to scope every Firestore query. Authentication is the foundation the rest of your backend leans on, so getting the state handling and rules right early pays off across the whole app. Treat identity and authorization as two separate jobs: FirebaseAuth answers who the user is, and Security Rules decide what that user is allowed to touch.

Frequently Asked Questions

Do I have to offer Sign in with Apple?

If your app offers any third-party or social sign-in option, Apple's App Store Review Guidelines require you to also offer Sign in with Apple. If you only use email/password or Apple's own login, you are fine. Because of this rule, apps that add Google or Facebook login almost always add Apple as well.

Why does Sign in with Apple need a nonce?

The nonce is a one-time random value whose hash is sent on the request and whose raw value is verified against Apple's returned identity token. It prevents replay attacks where an intercepted token could be reused. Firebase requires the raw nonce when building the Apple credential, so generate it before the request and keep it until you construct the credential.

How do I keep the user logged in between launches?

FirebaseAuth persists the session by default, so the user stays signed in across app launches until they sign out or the token is revoked. Your auth state listener will report the existing user on launch, so your UI can show the signed-in state immediately without asking them to log in again.

Should I use closures or async/await for Auth calls?

Both work, but async/await produces cleaner SwiftUI code. Wrap calls in a Task and use try await Auth.auth().signIn(...) with do/catch for errors. This avoids deeply nested completion handlers and integrates naturally with SwiftUI button actions and .task modifiers.

How do I restrict Firestore data to the signed-in user?

Authentication alone does not restrict data. You must write Firebase Security Rules that check request.auth.uid against the document's owner field or path. For example, allow read/write only if request.auth.uid equals the userId stored on the document. Without rules, an authenticated user could read others' data.

Why did Apple only give me the user's name once?

Apple returns the full name and email only on the first successful authorization for your app. On later sign-ins those fields are nil. Capture and persist them (for example to Firestore) during that first sign-in, because you cannot retrieve them from Apple again afterward.