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 andTODOcomments 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:
- In Finder or Terminal, navigate to the
ios-starter/swift-starterdirectory. - Double-click
FriendlyChatSwift.xcodeprojto open the project in Xcode. - Notice that Xcode automatically resolves Swift Package Manager dependencies in the background (downloading
firebase-ios-sdkv12+ from GitHub). - 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
- Sign into the Firebase console using your Google Account.
- Click the button to create a new project, and then enter a project name (for example,
FriendlyChat).
- Click Continue.
- If prompted, review and accept the Firebase terms, and then click Continue.
- (Optional) Enable AI assistance in the Firebase console (called "Gemini in Firebase").
- For this codelab, you do not need Google Analytics, so toggle off the Google Analytics option.
- 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:
- In the Firebase console, select to upgrade your plan.
- 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
- From the Project Overview screen, click the iOS icon to launch the app setup workflow.
- Enter the Bundle ID:
com.google.firebase.codelab.FriendlyChatSwift - Click Register app.
Add GoogleService-Info.plist to your Xcode project
- Click Download GoogleService-Info.plist to save the configuration file.
- In Xcode, drag the downloaded
GoogleService-Info.plistfile into theFriendlyChatSwiftmain group in the Project Navigator. - When prompted, ensure Copy items if needed is checked and the
FriendlyChatSwifttarget 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:
- In the Firebase console, go to Security > Authentication, then click Get started.
- Select the Sign-in method tab.
- 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:
- In the Firebase console, go to Databases & Storage > Firestore Database, then click Create database.
- Select a location and click Next.
- Select Start in test mode and click Create.
- Select the Rules tab, and update the security rules to:
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.rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /messages/{messageId} { allow read, write: if request.auth != null; } } } - 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
- Click the Run button in Xcode.
- In the app simulator, click Sign Up, enter a Display Name, Email, and Password, and tap SIGN UP.
- 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:
- In the Firebase console, go to Firestore Database and select the Data tab.
- Click Start collection.
- Enter
messagesfor the Collection ID, then click Next. - Leave the Document ID set to auto-generate (or click Auto-ID).
- Add the following fields to the document:
text(type:string, value:Hello)displayName(type:string, value:anonymous)
- Click Save.
- 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
- Click the Run button in Xcode.
- Sign in to your account.
- Type a message in the bottom text field and tap the Send icon.
- Watch the message instantly appear in your simulator!
8. Store and receive images
Set up Cloud Storage for Firebase
- In the Firebase console, go to Databases & Storage > Storage, then click Get started.
- Select Start in test mode and click Next.
- 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 usingStorage.storage().reference(forURL:).
Test image messages
- Click the Run button in Xcode.
- Sign in to your account.
- 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, andScrollViewReaderauto-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.