Firebase Firestore iOS Setup Guide
⛔️ CRITICAL RULE: NO FirebaseFirestoreSwift ⛔️
UNDER NO CIRCUMSTANCES should you import, link against, or configure a project
to use FirebaseFirestoreSwift.
As of Firebase SDK v11+, all Swift-specific features (including @DocumentID,
@ServerTimestamp, and Codable support) have been fully merged into the main
FirebaseFirestore module.
- NEVER add
.external(name: "FirebaseFirestoreSwift")or similar to SPM or Xcode configurations. - NEVER write
import FirebaseFirestoreSwiftin any Swift file. - ONLY use
import FirebaseFirestore.
This is a zero-tolerance constraint. Using FirebaseFirestoreSwift is
fundamentally incorrect and unacceptable.
⛔️ CRITICAL RULE: NO INLINE INITIALIZATION ⛔️
NEVER write let db = Firestore.firestore() as an inline class or struct
property if there is ANY chance the object is instantiated before
FirebaseApp.configure() executes in the app root.
- FATAL CRASH:
@Observable class DataManager { let db = Firestore.firestore() }initialized as a@Statein the App root. - SAFE PATTERN: Initialize
Firestore.firestore()lazily (lazy var db = Firestore.firestore()) OR explicitly initialize the manager afterFirebaseApp.configure()finishes.
1. Import and Initialize
Ensure you have installed the FirebaseFirestore SDK. Use the
xcode-project-setup skill to automate adding the SPM dependency to the Xcode
project.
import FirebaseFirestoreInitialize an instance of Cloud Firestore:
let db = Firestore.firestore()2. Type-Safe Data Models (Codable)
To leverage modern Swift data modeling, define your data as Codable structs.
The main FirebaseFirestore module automatically supports mapping these types.
struct User: Codable {
@DocumentID var id: String?
var firstName: String
var lastName: String
var born: Int
}3. Writing Data (Modern Concurrency & Codable)
Using async/await and Codable ensures type safety and avoids callback hell.
let user = User(firstName: "Ada", lastName: "Lovelace", born: 1815)
do {
// Add a new document with a generated ID using Codable
let ref = try db.collection("users").addDocument(from: user)
print("Document added with ID: \(ref.documentID)")
} catch {
print("Error adding document: \(error)")
}4. Reading Data (Modern Concurrency & Codable)
do {
let querySnapshot = try await db.collection("users").getDocuments()
// Map documents to the User struct automatically
let users = querySnapshot.documents.compactMap { document in
try? document.data(as: User.self)
}
for user in users {
print("Found user: \(user.firstName) \(user.lastName)")
}
} catch {
print("Error getting documents: \(error)")
}5. Realtime Listeners in SwiftUI (Lifecycle Best Practices)
When implementing Firestore realtime listeners (addSnapshotListener) within a
SwiftUI application, you MUST tie the listener lifecycle to the view's
identity using .task(id:), NOT .onDisappear.
⛔️ UNSAFE PATTERN (.onDisappear)
Presenting a .sheet or .fullScreenCover can trigger the underlying view's
onDisappear method. If you stop your listener here, the feed will stop
updating while the sheet is open, and won't resume when it's dismissed.
✅ SAFE PATTERN (.task with deinit)
Because addSnapshotListener is a synchronous call, placing it inside a .task
means the task completes immediately. This breaks SwiftUI's automatic
cancellation mechanism.
To safely manage traditional Firebase listeners in SwiftUI, you must use
deinit to handle memory cleanup when the view is destroyed, and
.task(id:) to handle data identity changes while the view is active.
import SwiftUI
import FirebaseFirestore
@MainActor
@Observable
final class DataManager {
private var listenerHandle: ListenerRegistration?
var data: [String] = []
func startListening(for userId: String) {
// 1. Clean up any existing listener to prevent duplicates if the ID changes
stopListening()
// 2. Start the regular listener and capture the handle
listenerHandle = Firestore.firestore().collection("users").document(userId).addSnapshotListener { snapshot, error in
// Handle updates
}
}
func stopListening() {
listenerHandle?.remove()
listenerHandle = nil
}
// 3. Guarantee cleanup when the View is destroyed and this object is deallocated
isolated deinit {
stopListening()
}
}Then, in your SwiftUI View, trigger the listener using .task(id:).
struct MyView: View {
@State private var manager = DataManager()
@Environment(AuthManager.self) var authManager
var body: some View {
List(manager.data, id: \.self) { item in
Text(item)
}
// .task(id:) automatically re-runs if the userId changes.
// The view model handles stopping the old listener and starting the new one.
.task(id: authManager.userId) {
if let userId = authManager.userId {
manager.startListening(for: userId)
} else {
manager.stopListening()
}
}
}
}