Add Firebase Firestore support to a Swift application through SwiftfulDataManagers framework.
See documentation in the parent repo: https://github.com/SwiftfulThinking/SwiftfulDataManagers
dependencies: [
.package(url: "https://github.com/SwiftfulThinking/SwiftfulDataManagersFirebase.git", branch: "main")
]import SwiftfulDataManagers
import SwiftfulDataManagersFirebase// DocumentSyncEngine — static path
let userSyncEngine = DocumentSyncEngine<UserModel>(
remote: FirebaseRemoteDocumentService(collectionPath: { "users" }),
managerKey: "user",
enableLocalPersistence: true,
logger: logManager
)
// DocumentSyncEngine — dynamic path
let settingsSyncEngine = DocumentSyncEngine<UserSettings>(
remote: FirebaseRemoteDocumentService(
collectionPath: { [weak authManager] in
guard let uid = authManager?.currentUserId else { return nil }
return "users/\(uid)/settings"
}
),
managerKey: "settings"
)
// CollectionSyncEngine — static path
let productsSyncEngine = CollectionSyncEngine<Product>(
remote: FirebaseRemoteCollectionService(collectionPath: { "products" }),
managerKey: "products",
enableLocalPersistence: true,
logger: logManager
)
// CollectionSyncEngine — dynamic path
let watchlistSyncEngine = CollectionSyncEngine<WatchlistItem>(
remote: FirebaseRemoteCollectionService(
collectionPath: { [weak authManager] in
guard let uid = authManager?.currentUserId else { return nil }
return "users/\(uid)/watchlist"
}
),
managerKey: "watchlist"
)// DocumentSyncEngine
try await userSyncEngine.startListening(documentId: "user_123")
try await userSyncEngine.saveDocument(user)
try await userSyncEngine.updateDocument(data: ["name": "John"])
let user = userSyncEngine.currentDocument
let user = try await userSyncEngine.getDocumentAsync()
userSyncEngine.stopListening()
// CollectionSyncEngine
await productsSyncEngine.startListening()
try await productsSyncEngine.saveDocument(product)
try await productsSyncEngine.updateDocument(id: "product_123", data: ["price": 29.99])
let products = productsSyncEngine.currentCollection
let product = productsSyncEngine.getDocument(id: "product_123")
let results = try await productsSyncEngine.getDocumentsAsync(buildQuery: { query in
query.where("category", isEqualTo: "electronics")
})
productsSyncEngine.stopListening()Details (Click to expand)
Firebase services use closures for collection paths, supporting both static and dynamic paths:
// Simple collection
FirebaseRemoteDocumentService<UserModel>(
collectionPath: { "users" }
)
// Creates: users/{documentId}
// Nested collection with hardcoded IDs
FirebaseRemoteCollectionService<CommentModel>(
collectionPath: { "posts/post123/comments" }
)
// Creates: posts/post123/comments/{documentId}// Path depends on runtime value (e.g., current user)
let watchlistSyncEngine = CollectionSyncEngine<WatchlistItem>(
remote: FirebaseRemoteCollectionService(
collectionPath: { [weak authManager] in
guard let uid = authManager?.currentUserId else { return nil }
return "users/\(uid)/watchlist"
}
),
managerKey: "watchlist"
)
// Multiple nesting levels
let repliesSyncEngine = CollectionSyncEngine<ReplyModel>(
remote: FirebaseRemoteCollectionService(
collectionPath: {
guard let postId = currentPostId,
let commentId = currentCommentId else {
return nil
}
return "posts/\(postId)/comments/\(commentId)/replies"
}
),
managerKey: "replies"
)Use cases:
- User-specific subcollections (favorites, settings, posts)
- Hierarchical data structures (comments, replies)
- Scoped collections per entity
- Engine initialization before authentication
Error handling:
When the closure returns nil, operations will throw FirebaseServiceError.collectionPathNotAvailable. This allows engines to be created before the path is available (e.g., before login), and operations will automatically fail with a clear error until the path becomes available.
Details (Click to expand)
Firebase docs: https://firebase.google.com/docs/firestore
- Firebase Console -> Build -> Firestore Database -> Create Database
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Allow authenticated users to read/write their own documents
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// Allow authenticated users to read all products, write if admin
match /products/{document=**} {
allow read: if request.auth != null;
allow write: if request.auth != null && request.auth.token.admin == true;
}
// Add more rules as needed
}
}dependencies: [
.package(url: "https://github.com/firebase/firebase-ios-sdk", from: "10.0.0"),
.package(url: "https://github.com/SwiftfulThinking/SwiftfulDataManagersFirebase.git", branch: "main")
]import Firebase
// In App init or AppDelegate
FirebaseApp.configure()Details (Click to expand)
FirebaseRemoteDocumentService provides real-time document updates:
func streamDocument(id: String) -> AsyncThrowingStream<T?, Error>FirebaseRemoteCollectionService follows a hybrid pattern used by CollectionSyncEngine:
// 1. Bulk load all documents first
let collection = try await service.getCollection()
// 2. Stream individual updates/deletions
func streamCollectionUpdates() -> (
updates: AsyncThrowingStream<T, Error>,
deletions: AsyncThrowingStream<String, Error>
)This pattern prevents unnecessary full collection re-fetches and efficiently handles individual document changes.
Full documentation and examples: https://github.com/SwiftfulThinking/SwiftfulDataManagers