FirebaseAPI for Swift is a Swift package that provides a simple interface to interact with Firebase services using gRPC.
This repository includes the googleapis repository as a submodule, which is used to generate the API client code for Firebase.
- ✅ Firestore API: Full support for Firestore operations (CRUD, queries, transactions, batches)
- ✅ Generic Transport: Support for any
ClientTransportimplementation from grpc-swift-2 - ✅ Swift 6 Ready: Full concurrency support with
async/awaitandSendable - ✅ Type-safe Encoding/Decoding: FirestoreEncoder and FirestoreDecoder for seamless Swift type conversion
- ✅ Property Wrappers:
@DocumentID,@ReferencePath,@ExplicitNullfor Firestore-specific behaviors - ✅ Retry Strategy: Built-in retry handling with exponential backoff
- Swift 6.2+
- macOS 15.0+ / iOS 18.0+ / watchOS 11.0+ / tvOS 18.0+ / visionOS 2.0+
Add FirebaseAPI to your Package.swift:
dependencies: [
.package(url: "https://github.com/1amageek/FirebaseAPI.git", from: "0.1.0")
]Then add it to your target dependencies:
.target(
name: "YourTarget",
dependencies: [
.product(name: "FirestoreAPI", package: "FirebaseAPI")
]
)import FirestoreAPI
import GRPCHTTP2TransportNIOPosix // or your preferred transport
// Create a transport (example using HTTP/2 with NIO)
let transport = HTTP2ClientTransport.Posix(
target: .ipv4(host: "firestore.googleapis.com", port: 443),
config: .defaults(transportSecurity: .tls)
)
// Initialize Firestore with generic transport
let firestore = Firestore(
projectId: "your-project-id",
transport: transport,
accessTokenProvider: yourAccessTokenProvider
)// Define your model
struct User: Codable {
@DocumentID var id: String
var name: String
var email: String
var createdAt: Timestamp
}
// Create a document
let userRef = firestore.collection("users").document("user123")
try await userRef.setData([
"name": "John Doe",
"email": "john@example.com",
"createdAt": Timestamp.now()
], firestore: firestore)
// Or use Codable
let user = User(id: "user123", name: "John Doe", email: "john@example.com", createdAt: .now())
try await userRef.setData(user, firestore: firestore)
// Read a document
let snapshot = try await userRef.getDocument(firestore: firestore)
if let data = snapshot.data() {
print("User data: \(data)")
}
// Or decode to Codable
let user: User? = try await userRef.getDocument(type: User.self, firestore: firestore)
// Update a document
try await userRef.updateData(["name": "Jane Doe"], firestore: firestore)
// Delete a document
try await userRef.delete(firestore: firestore)// Simple query
let usersRef = firestore.collection("users")
let snapshot = try await usersRef
.where("age" >= 18)
.where("city" == "Tokyo")
.orderBy("name", descending: false)
.limit(10)
.getDocuments(firestore: firestore)
for doc in snapshot.documents {
print(doc.data())
}
// Query with Codable
let users: [User] = try await usersRef
.where("age" >= 18)
.getDocuments(type: User.self, firestore: firestore)try await firestore.runTransaction { transaction in
// Read documents
let userDoc = firestore.document("users/user123")
let snapshot = try await transaction.get(documentReference: userDoc)
guard let balance = snapshot.data()?["balance"] as? Int else {
throw FirestoreError.notFound
}
// Write operations
transaction.updateData(["balance": balance - 100], forDocument: userDoc)
return balance - 100
}let batch = firestore.batch()
let user1 = firestore.document("users/user1")
let user2 = firestore.document("users/user2")
batch.setData(["name": "Alice"], forDocument: user1)
batch.updateData(["lastLogin": Timestamp.now()], forDocument: user2)
batch.deleteDocument(document: firestore.document("users/user3"))
try await batch.commit()struct Post: Codable {
@DocumentID var id: String
@ReferencePath var path: String
@ExplicitNull var deletedAt: Date?
var title: String
var content: String
var authorRef: DocumentReference
}
// @DocumentID: Automatically populated with document ID during decoding
// @ReferencePath: Automatically populated with document path
// @ExplicitNull: Encodes as NSNull instead of omitting the fieldFirebaseAPI uses a generic Transport parameter that conforms to ClientTransport from grpc-swift-2. This design allows:
- Flexibility: Use any transport implementation (HTTP/2, NIO-based, custom)
- Type Safety: Transport type is known at compile time for optimal performance
- Testability: Easy to mock transport for unit tests
public final class Firestore<Transport: ClientTransport>: Sendable {
internal let transport: Transport
// ...
}The library uses Firestore<Transport: ClientTransport> instead of any ClientTransport because:
- gRPC Client Requirements:
GRPCClient<Transport>requires a concrete type parameter - Swift Type System: Existential types (
any Protocol) cannot conform to protocols withSelfrequirements - Performance: Generic types are resolved at compile time, avoiding runtime overhead
To develop this library, you need:
- Swift 6.2+
- Protocol Buffer compiler (
protoc) - gRPC Swift plugins
This repository includes the googleapis as a submodule. To regenerate the Firestore proto files:
mkdir -p Sources/FirestoreAPI/Proto
cd googleapis
protoc \
./google/firestore/v1/*.proto \
./google/api/field_behavior.proto \
./google/api/resource.proto \
./google/longrunning/operations.proto \
./google/rpc/status.proto \
./google/type/latlng.proto \
--swift_out=../Sources/FirestoreAPI/Proto \
--grpc-swift_out=../Sources/FirestoreAPI/Proto \
--swift_opt=Visibility=Public \
--grpc-swift_opt=Visibility=PublicThe test suite uses Swift Testing framework (not XCTest):
swift testAll 67 tests should pass:
- Reference Path Tests: 9 tests
- Query Predicate Tests: 6 tests
- Firestore Encoder Tests: 21 tests
- Firestore Decoder Tests: 23 tests
- Listen API Tests: 8 tests
- ✅ Firestore Encoder/Decoder for all supported types
- ✅ Document reference path generation
- ✅ Query predicates and operators
- ✅ Property wrappers (@DocumentID, @ReferencePath, @ExplicitNull)
- ✅ Real-time listener response processing
- ✅ Mock transport for testing without network calls
- grpc-swift-2: gRPC core and protocols
- grpc-swift-protobuf: Protobuf serialization
- swift-protobuf: Protocol Buffer runtime
- swift-log: Logging infrastructure
This library has been migrated to grpc-swift-2.x. Key changes:
HPACKHeaders→MetadataClientCall→ClientRequestwith new API- Direct
GRPCClientcreation instead of connection pooling - Bidirectional streaming now fully supported for real-time listeners
The library now supports real-time listeners for documents and queries using bidirectional streaming:
// Listen to document changes
let docRef = firestore.collection("users").document("user123")
let stream = try await docRef.addSnapshotListener(firestore: firestore)
for try await snapshot in stream {
if snapshot.exists {
print("Document updated: \(snapshot.data())")
} else {
print("Document deleted or doesn't exist")
}
}// Listen to query changes
let query = firestore.collection("users").where("age" >= 18)
let stream = try await query.addSnapshotListener(firestore: firestore)
for try await snapshot in stream {
print("Query results updated: \(snapshot.documents.count) documents")
for doc in snapshot.documents {
print(doc.data())
}
}Note: The stream will continue until cancelled or an error occurs. Use task cancellation to stop listening:
let task = Task {
for try await snapshot in stream {
// Process snapshot
}
}
// Later: stop listening
task.cancel()This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.