Chapter 08 · Firebase Firestore
Subchapter 8.5
references/enterprise/ios_setup.mdMarkdown6 KBView on GitHub
This guide walks you through using the Cloud Firestore SDK in your iOS app using Swift.
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.
.external(name: "FirebaseFirestoreSwift") or similar to SPM or
Xcode configurations.import FirebaseFirestoreSwift in any Swift file.import FirebaseFirestore.This is a zero-tolerance constraint. Using FirebaseFirestoreSwift is
fundamentally incorrect and unacceptable.
NEVER write let db = Firestore.firestore() or Firestore.firestore(database:)
as an inline class or struct property if there is ANY chance the object is
instantiated before FirebaseApp.configure() executes in the app root.
@Observable class DataManager { let db = Firestore.firestore() } initialized
as a @State in the App root.Firestore.firestore() lazily
(lazy var db = Firestore.firestore()) OR explicitly initialize the manager
after FirebaseApp.configure() finishes.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. CRITICAL: Enterprise databases
require a custom database ID and cannot use the (default) instance.
// Replace "your-enterprise-database-id" with your actual database ID
let db = Firestore.firestore(database: "your-enterprise-database-id")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
}The operations are identical to standard Firestore, but ensure you use the db
instance initialized with your Enterprise database ID.
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)")
}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)")
}Firestore Enterprise supports Pipeline operations for complex queries.
let pipeline = db.pipeline()// Return all documents across all collections in the database
let results = try await db.pipeline().database().execute()
// Filtered query
let results = try await db.pipeline()
.collection("cities")
.where(Field("name").equal(Constant("Toronto")))
.execute()
// Compound query
let results = try await db.pipeline()
.collection("books")
.where(Field("rating").equal(5) && Field("published").lessThan(1900))
.execute()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.
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.
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
// Note: Using the global default instance here, make sure to use your enterprise instance if applicable
// For enterprise, you might need to pass the db instance or use a shared manager.
listenerHandle = Firestore.firestore(database: "your-enterprise-database-id").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()
}
}