Firebase iOS codelab: FriendlyChat

1. Overview

Welcome to the FriendlyChat codelab for iOS! In this codelab, you will learn how to build a full-featured real-time chat iOS application using Firebase and modern Swift features like SwiftUI, Swift Concurrency, and the Observation framework.

What you'll learn

  • Manage state with the modern Observation framework (@Observable).
  • Authenticate users using Firebase Authentication.
  • Synchronize chat messages in real time using Cloud Firestore.
  • Store and serve images using Cloud Storage for Firebase.
  • Access Firebase APIs using modern Swift Concurrency features.

What you'll need

  • The latest version of Xcode
  • An iOS simulator or physical test device
  • A Google Account to create and manage your Firebase project

2. Get the sample code

Clone the GitHub repository from the command line:

$ git clone https://github.com/firebase/codelab-friendlychat-ios

The codelab-friendlychat-ios repository contains several sample projects:

  • ios-starter/swift-starter — The starting Xcode project containing the SwiftUI layout skeleton and TODO comments where you will implement app features with Firebase.
  • ios/swift — The completed Xcode project with the finished sample application.

3. Build the starter app

Build and run the starter application to familiarize yourself with the user interface:

  1. In Finder or Terminal, navigate to the ios-starter/swift-starter directory.
  2. Double-click FriendlyChatSwift.xcodeproj to open the project in Xcode.
  3. Notice that Xcode automatically resolves Swift Package Manager dependencies in the background (downloading firebase-ios-sdk v12+ from GitHub).
  4. Select an iOS Simulator (for example, iPhone 17 Pro) and click the Run button (cmd+R).

After a few seconds, the FriendlyChat login screen will appear. At this point, the login buttons and message feeds are placeholders. You will connect Firebase to the app in the following steps.

4. Set up a Firebase project

Create a new Firebase project

  1. Sign into the Firebase console using your Google Account.
  2. Click the button to create a new project, and then enter a project name (for example, FriendlyChat).
  3. Click Continue.
  4. If prompted, review and accept the Firebase terms, and then click Continue.
  5. (Optional) Enable AI assistance in the Firebase console (called "Gemini in Firebase").
  6. For this codelab, you do not need Google Analytics, so toggle off the Google Analytics option.
  7. Click Create project, wait for your project to provision, and then click Continue.

Upgrade your Firebase pricing plan

To use Cloud Storage for Firebase, your Firebase project needs to be on the pay-as-you go (Blaze) pricing plan, which means it's linked to a Cloud Billing account.

  • A Cloud Billing account requires a payment method, like a credit card.
  • If you're new to Firebase and Google Cloud, check if you're eligible for a $300 credit and a Free Trial Cloud Billing account.
  • If you're doing this codelab as part of an event, ask your organizer if there are any Cloud credits available.

To upgrade your project to the Blaze plan, follow these steps:

  1. In the Firebase console, select to upgrade your plan.
  2. Select the Blaze plan. Follow the on-screen instructions to link a Cloud Billing account to your project.
    If you needed to create a Cloud Billing account as part of this upgrade, you might need to navigate back to the upgrade flow in the Firebase console to complete the upgrade.

Connect your iOS app

  1. From the Project Overview screen, click the iOS icon to launch the app setup workflow.
  2. Enter the Bundle ID:
    com.google.firebase.codelab.FriendlyChatSwift
    
  3. Click Register app.

Add GoogleService-Info.plist to your Xcode project

  1. Click Download GoogleService-Info.plist to save the configuration file.
  2. In Xcode, drag the downloaded GoogleService-Info.plist file into the FriendlyChatSwift main group in the Project Navigator.
  3. When prompted, ensure Copy items if needed is checked and the FriendlyChatSwift target is selected, then click Finish.

Configure Firebase in FriendlyChatSwiftApp.swift

Finally, initialize Firebase with your project configuration when your SwiftUI application launches. Open FriendlyChatSwiftApp.swift and replace the TODO placeholders with import FirebaseCore and FirebaseApp.configure():

import SwiftUI
import FirebaseCore

@main
struct FriendlyChatSwiftApp: App {
  init() {
    FirebaseApp.configure()
  }

  var body: some Scene {
    WindowGroup {
      ContentView()
    }
  }
}

5. Authenticate users

Configure authentication in the Firebase console

Before you can authenticate users, you must first enable your auth provider of choice in the Firebase console. This tutorial uses Email/Password authentication:

  1. In the Firebase console, go to Security > Authentication, then click Get started.
  2. Select the Sign-in method tab.
  3. Click Email/Password, click the Enable toggle switch, and click Save.

Secure Cloud Firestore with authentication rules

Next, require users to be authenticated before reading or writing messages:

  1. In the Firebase console, go to Databases & Storage > Firestore Database, then click Create database.
  2. Select a location and click Next.
  3. Select Start in test mode and click Create.
  4. Select the Rules tab, and update the security rules to:
    rules_version = '2';
    
    service cloud.firestore {
      match /databases/{database}/documents {
        match /messages/{messageId} {
          allow read, write: if request.auth != null;
        }
      }
    }
    
    These rules allow any authenticated user to read or write messages in the database, which is suitable for learning purposes. Learn more about security rules in the security rules documentation.
  5. Click Publish.

Observe authentication state in UserViewModel.swift

In SwiftUI, observing Firebase Auth state reactively ensures your UI switches smoothly between the login screen and the chat interface.

Open ViewModels/UserViewModel.swift and replace the TODO in init() to observe auth state changes using the authStateChanges AsyncSequence:

  init() {
    authTask = Task {
      for await user in Auth.auth().authStateChanges {
        self.user = user
      }
    }
  }

Implement sign-in, sign-up, and sign-out with Swift Concurrency

In UserViewModel.swift, replace the remaining TODO comments with modern async/await Authentication methods:

  func signIn(email: String, password: String) async {
    errorMessage = nil
    if email.isEmpty || password.isEmpty {
      showError("Please enter both email and password.")
      return
    }
    do {
      try await Auth.auth().signIn(withEmail: email, password: password)
    } catch {
      showError(error.localizedDescription)
    }
  }

  func signUp(email: String, password: String, displayName: String) async {
    errorMessage = nil
    if email.isEmpty || password.isEmpty {
      showError("Please enter both email and password.")
      return
    }
    do {
      let result =
        try await Auth.auth().createUser(withEmail: email, password: password)
      let changeRequest = result.user.createProfileChangeRequest()
      changeRequest.displayName = displayName.isEmpty ? email : displayName
      try await changeRequest.commitChanges()
      self.user = Auth.auth().currentUser
    } catch {
      showError(error.localizedDescription)
    }
  }

  func updateDisplayName(_ displayName: String) async {
    guard let currentUser = Auth.auth().currentUser else { return }
    do {
      let changeRequest = currentUser.createProfileChangeRequest()
      changeRequest.displayName = displayName
      try await changeRequest.commitChanges()
      self.user = Auth.auth().currentUser
    } catch {
      showError(error.localizedDescription)
    }
  }

  func signOut() {
    do {
      try Auth.auth().signOut()
    } catch {
      showError(error.localizedDescription)
    }
  }

Test user authentication

  1. Click the Run button in Xcode.
  2. In the app simulator, click Sign Up, enter a Display Name, Email, and Password, and tap SIGN UP.
  3. You should be automatically authenticated and navigated to the empty messaging screen!

6. Read from Cloud Firestore

Add sample messages in the Firebase console

Populate the database with sample messages:

  1. In the Firebase console, go to Firestore Database and select the Data tab.
  2. Click Start collection.
  3. Enter messages for the Collection ID, then click Next.
  4. Leave the Document ID set to auto-generate (or click Auto-ID).
  5. Add the following fields to the document:
    • text (type: string, value: Hello)
    • displayName (type: string, value: anonymous)
  6. Click Save.
  7. Optionally click Add document to add more sample messages.

Synchronize messages in FriendlyMessageViewModel.swift

In SwiftUI, an @Observable model drives declarative UI updates. Open ViewModels/FriendlyMessageViewModel.swift and implement startListening() and stopListening() using the snapshots AsyncSequence:

  func startListening() {
    stopListening()
    listenerTask = Task {
      let db = Firestore.firestore()
      do {
        for try await snapshot in db.collection("messages").snapshots {
          self.messages = snapshot.documents.compactMap { document in
            try? document.data(as: FriendlyMessage.self)
          }
        }
      } catch {
        print("Error listening for messages: \(error)")
      }
    }
  }

  func stopListening() {
    listenerTask?.cancel()
    listenerTask = nil
    messages.removeAll()
  }

How SwiftUI renders and auto-scrolls messages

In Views/ContentView.swift, notice how ScrollViewReader and LazyVStack render chat bubbles and automatically scroll to the newest message whenever messages.count changes:

ScrollViewReader { scrollViewReader in
  ScrollView {
    LazyVStack(spacing: 12) {
      ForEach(messageViewModel.messages) { message in
        FriendlyMessageView(friendlyMessage: message)
          .id(message.id)
      }
    }
    .padding(.horizontal)
    .onChange(of: messageViewModel.messages.count) { _, count in
      guard count > 0,
            let lastId = messageViewModel.messages.last?.id else { return }
      withAnimation(.easeInOut) {
        scrollViewReader.scrollTo(lastId, anchor: .bottom)
      }
    }
  }
}

By tying database state directly to UI state through the view model, SwiftUI allows app code to cleanly map transitions to animations. In the next step you will write data to the database from the app to observe this animation in action.

7. Send messages

Implement sendMessage with Swift Concurrency

When you add a document to a collection using addDocument(), Cloud Firestore generates a unique ID for each chat message.

In ViewModels/FriendlyMessageViewModel.swift, replace the TODO in sendMessage(text:imageUrl:) with an async throws method:

  func sendMessage(text: String?, imageUrl: String?) async throws {
    guard let currentUser = Auth.auth().currentUser else { return }
    let message = FriendlyMessage(
      text: text,
      displayName: currentUser.displayName ?? currentUser.email ?? "Anonymous",
      imageUrl: imageUrl,
      userId: currentUser.uid
    )
    let db = Firestore.firestore()
    _ = try db.collection("messages").addDocument(from: message)
  }

Test sending messages

  1. Click the Run button in Xcode.
  2. Sign in to your account.
  3. Type a message in the bottom text field and tap the Send icon.
  4. Watch the message instantly appear in your simulator!

8. Store and receive images

Set up Cloud Storage for Firebase

  1. In the Firebase console, go to Databases & Storage > Storage, then click Get started.
  2. Select Start in test mode and click Next.
  3. Accept the default storage location and click Done.

Native image picking with SwiftUI PhotosPicker

Modern SwiftUI (iOS 16+) provides native photo selection via PhotosPicker from import PhotosUI—no UIKit delegate bridges or UIImagePickerController wrappers required!

In Views/FooterView.swift, observe how PhotosPicker binds selected images:

PhotosPicker(selection: $selectedItem, matching: .images) {
  Image(systemName: "photo.on.rectangle.angled")
    .font(.system(size: 26))
    .foregroundStyle(.blue)
    .accessibilityLabel("Select photo")
}
.onChange(of: selectedItem) { _, newItem in
  Task {
    if let data = try? await newItem?.loadTransferable(type: Data.self) {
      await uploadAndSendImage(data: data)
    }
  }
}

Implement image uploading

When a photo is selected, upload the image data to Cloud Storage and save the public download URL in Cloud Firestore.

In Views/FooterView.swift, replace the TODO in uploadAndSendImage(data:):

  private func uploadAndSendImage(data: Data) async {
    guard let uid = Auth.auth().currentUser?.uid else { return }
    isUploading = true
    defer {
      isUploading = false
      selectedItem = nil
    }
    do {
      let filename = "\(uid)/\(UUID().uuidString).jpg"
      let storageRef =
        Storage.storage().reference().child("images").child(filename)
      let metadata = StorageMetadata()
      metadata.contentType = "image/jpeg"

      _ = try await storageRef.putDataAsync(data, metadata: metadata)
      let downloadURL = try await storageRef.downloadURL()
      try await viewModel.sendMessage(
        text: nil, imageUrl: downloadURL.absoluteString)
    } catch {
      print("Error uploading image: \(error.localizedDescription)")
    }
  }

Image sharing with Cloud Storage

In Views/FriendlyMessageImageView.swift, image URLs are loaded asynchronously using Swift Concurrency and the Cloud Storage SDK:

  • Standard HTTP/HTTPS download URLs are retrieved asynchronously via URLSession.
  • Google Cloud Storage URLs (for example, gs://...) are fetched using Storage.storage().reference(forURL:).

Test image messages

  1. Click the Run button in Xcode.
  2. Sign in to your account.
  3. Tap the Photo icon, choose an image from the library, and watch it upload and appear in the chat!

9. Conclusion

Congratulations, you have successfully built a real-time iOS chat application using Swift and Firebase!

What you've learned

  • Swift Package Manager (SPM) Xcode integration for firebase-ios-sdk.
  • SwiftUI declarative layouts, PhotosPicker, and ScrollViewReader auto-scrolling.
  • Swift Concurrency (async/await) across Authentication, Cloud Firestore, and Cloud Storage.
  • Email/Password & Anonymous Authentication with reactive state observation.
  • Cloud Firestore real-time snapshot listening and automated ID writes.
  • Cloud Storage for Firebase binary uploads and public download URLs.

Learn more