Handling Firebase asynchronous callbacks with Combine framework.
To run the example project, clone the repo, and run pod install
from the Example directory first. See Usage below for more information.
- Xcode 11.3+ | Swift 5.1+
- iOS 13.0+ | tvOS 13.0+ | macOS 10.15+
CombineFirebase
is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'CombineFirebase/Firestore'
pod 'CombineFirebase/RemoteConfig'
pod 'CombineFirebase/Database'
pod 'CombineFirebase/Storage'
pod 'CombineFirebase/Auth'
pod 'CombineFirebase/Functions'
CombineFirebase
is available through Swift Package Manager in beta status. To install it, you must have Xcode 12.0 and above, simply add CombineFirebase
to an existing Xcode project as a package dependency:
- From the File menu, select Swift Packages > Add Package Dependency...
- Enter https://github.com/rever-ai/CombineFirebase into the package repository URL text field.
- Xcode should choose updates package up to the next version option by default.
import CombineFirebase
import Firebase
import Combine
Basic write operation
var cancelBag = Set<AnyCancellable>()
func setUserData() {
let ref = Database.database().reference()
ref.child("users")
.child("1")
.setValue(["username": "Arnonymous"])
.sink { _ in
print("Document successfully updated")
}.store(in: &cancelBag)
}
// https://firebase.google.com/docs/database/ios/read-and-write#basic_write
Listen for value events
var cancelBag = Set<AnyCancellable>()
func listenForValueEvent() {
let ref = Database.database().reference()
ref.child("users")
.child("1")
.publisher(.value)
.receive(on: RunLoop.main)
.sink { snapshot in
print("Value:\(snapshot.value)")
}.store(in: &cancelBag)
}
// https://firebase.google.com/docs/database/ios/read-and-write#listen_for_value_events
Read data once
var cancelBag = Set<AnyCancellable>()
func readDataOnce() {
let ref = Database.database().reference()
ref.child("users")
.child("1")
.observeSingleEvent(.value)
.receive(on: RunLoop.main)
.sink { snapshot in
print("Value:\(snapshot.value)")
}.store(in: &cancelBag)
}
// https://firebase.google.com/docs/database/ios/read-and-write#read_data_once
Update specific fields
var cancelBag = Set<AnyCancellable>()
func updateFields() {
let ref = Database.database().reference()
let childUpdates = ["/posts/\(key)": post,
"/user-posts/\(userID)/\(key)/": post]
ref.updateChildValues(childUpdates)
.receive(on: RunLoop.main)
.sink { _ in
// Success
}.store(in: &cancelBag)
}
// https://firebase.google.com/docs/database/ios/read-and-write#update_specific_fields
Delete data
var cancelBag = Set<AnyCancellable>()
func deleteData() {
let ref = Database.database().reference()
ref.removeValue()
.receive(on: RunLoop.main)
.sink { _ in
// Success
}.store(in: &cancelBag)
}
// https://firebase.google.com/docs/database/ios/read-and-write#delete_data
Save data as transactions
var cancelBag = Set<AnyCancellable>()
func saveDataAsTransaction() {
let ref = Database.database().reference()
ref.runTransactionBlock { currentData in
// TransactionResult
}.sink { _ in
// Success
}.store(in: &cancelBag)
}
// https://firebase.google.com/docs/database/ios/read-and-write#save_data_as_transactions
Setting data
var cancelBag = Set<Cancellable>()
let db = Firestore.firestore()
struct City: Codable {
var name: String? = nil
var state: String? = nil
var country: String? = nil
var capital: String? = nil
var population: Int? = nil
// local variable
var id: String? = nil
}
func setSanFranciscoData(city: City) {
let onErrorCompletion: ((Subscribers.Completion<Error>) -> Void)? = { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): print("βοΈ failure: \(error)")
}
}
let onValue: (Void) -> Void = {
print("β
value")
}
// Add a new document in collection "cities"
(db.collection("cities")
.document("SF")
.setData(from: city) as AnyPublisher<Void, Error>) // Note: you can use (as Void) for simple setData({})
.sink(receiveCompletion: onErrorCompletion, receiveValue: onValue)
.store(in: &cancelBag)
}
// Add a new document with a generated id.
func addSanFranciscoDocument(city: City) {
(db.collection("cities")
.addDocument(data: [
"name": "San Francisco",
"state": "CA",
"country": "USA",
"capital": false,
"population": 860000
]) as AnyPublisher<DocumentReference, Error>)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): print("βοΈ failure: \(error)")
}) { ref in
print("Document added with ID: \(ref.documentID)")
}
.store(in: &cancelBag)
}
// Set the "capital" field of the city 'SF'
func updateSanFranciscoDocument() {
(db.collection("cities")
.document("SF")
.updateData([
"capital": true
]) as AnyPublisher<Void, Error>)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): print(i"βοΈ failure: \(error)")
}) { _ in }
.store(in: &cancelBag)
}
// https://firebase.google.com/docs/firestore/manage-data/add-data
Get a document
func getDocument() {
db.collection("cities")
.document("SF")
.getDocument()
.sink(receiveCompletion: { (completion) in
switch completion {
case .finished: print("π finished")
case .failure(let error): print("βοΈ failure: \(error)")
}
}) { document in
print("Document data: \(document.data())")
}
.store(in: &cancelBag)
}
func getDocumentAsObject() {
db.collection("cities")
.document("SF")
.getDocument(as: City.self)
.sink(receiveCompletion: { (completion) in
switch completion {
case .finished: print("π finished")
case .failure(let error): print("βοΈ failure: \(error)")
}
}) { city in
print("City: \(city)")
}
.store(in: &cancelBag)
}
// https://firebase.google.com/docs/firestore/query-data/get-data
Get Realtime updates
let db = Firestore.firestore()
// Document
func listenDocument() {
db.collection("cities")
.document("SF")
.publisher()
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): print("βοΈ failure: \(error)")
}
}) { document in
print("Document data: \(document.data())")
}
.store(in: &cancelBag)
}
var cityDocumentSnapshotMapper: (DocumentSnapshot) throws -> City? {
{
var city = try $0.data(as: City.self)
city.id = $0.documentID
return city
}
}
func listenDocumentAsObject() {
db.collection("cities")
.document("SF")
.publisher(as: City.self, documentSnapshotMapper: cityDocumentSnapshotMapper)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): print("βοΈ failure: \(error)")
}
}) { city in
print("City: \(city)")
}
.store(in: &cancelBag)
}
// Collection
func listenCollection() {
db.collection("cities")
.publisher()
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): print("βοΈ failure: \(error)")
}
}) { snapshot in
print("collection data: \(snapshot.documents)")
}.store(in: &cancelBag)
}
func listenCollectionAsObject() {
db.collection("cities")
.publisher(as: City.self, documentSnapshotMapper: cityDocumentSnapshotMapper)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): print("βοΈ failure: \(error)")
}
}) { cities in
print("Cities: \(cities)")
}.store(in: &cancelBag)
}
// https://firebase.google.com/docs/firestore/query-data/listen
Batched writes
var cancelBag = Set<AnyCancellable>()
func batchWrite() {
let db = Firestore.firestore()
// Get new write batch
let batch = db.batch()
// Update the population of 'SF'
let sfRef = db.collection("cities").document("SF")
batch.updateData(["population": 1000000 ], forDocument: sfRef)
// Commit the batch
batch.commit()
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): print("βοΈ failure: \(error)")
}
}) { _ in}
.store(in: &cancelBag)
}
// https://firebase.google.com/docs/firestore/manage-data/transactions
Transactions
var cancelBag = Set<AnyCancellable>()
func transaction() {
let db = Firestore.firestore()
let sfReference = db.collection("cities").document("SF")
(db.runTransaction { transaction in
let sfDocument = try transaction.getDocument(sfReference)
guard let oldPopulation = sfDocument.data()?["population"] as? Int else {
let error = NSError(
domain: "AppErrorDomain",
code: -1,
userInfo: [
NSLocalizedDescriptionKey: "Unable to retrieve population from snapshot \(sfDocument)"
]
)
throw error
}
transaction.updateData(["population": oldPopulation + 1], forDocument: sfReference)
return nil
} as AnyPublisher<Any?, Error>)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): print("βοΈ failure: \(error)")
}
}) { _ in
print("Transaction successfully committed!")
}
.store(in: &cancelBag)
}
// https://firebase.google.com/docs/firestore/manage-data/transactions
Fetch
// TimeInterval is set to expirationDuration here, indicating the next fetch request will use
// data fetched from the Remote Config service, rather than cached parameter values, if cached
// parameter values are more than expirationDuration seconds old. See Best Practices in the
// README for more information.
var cancelBag = Set<AnyCancellable>()
func fetchRemoteConfig() {
(RemoteConfig.remoteConfig()
.fetch(withExpirationDuration: TimeInterval(expirationDuration), activateFetched: true) as AnyPublisher<RemoteConfigFetchStatus, Error>)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): print("βοΈ failure: \(error)")
}
}) { status in
print("Config fetched! with success:\(status == .success)")
}
.store(in: &cancelBag)
}
// https://firebase.google.com/docs/remote-config/ios
Upload
var cancelBag = Set<AnyCancellable>()
let reference = Storage.storage()
.reference(forURL: "\(your_firebase_storage_bucket)/images/space.jpg")
let data: Data // Upload data
(reference.putData(data) as AnyPublisher<StorageMetadata, Error>)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): // Uh-oh, an error occurred!
}
}) { metadata in
// Success
}
.store(in: &cancelBag)
let fileURL: URL // Upload file
(reference.putFile(from: fileURL) as AnyPublisher<StorageMetadata, Error>)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): // Uh-oh, an error occurred!
}
}) { metadata in
// Success
}
.store(in: &cancelBag)
Observe events
var cancelBag = Set<AnyCancellable>()
let reference = Storage.storage()
.reference(forURL: "\(your_firebase_storage_bucket)/images/space.jpg")
let fileURL: URL // Upload file
let uploadTask = reference.putFile(from: fileURL)
// Listen for state changes
uploadTask.publisher(.progress)
.sink(receiveCompletion: { _ in
print("π finished")
}) { snapshot in
if let error = snapshot.error {
print("error: \(error)")
}
// Upload reported progress
let percentComplete = 100.0 * Double(snapshot.progress?.completedUnitCount ?? 0)
/ Double(snapshot.progress.totalUnitCount ?? 1)
}
.store(in: &cancelBag)
Download
var cancelBag = Set<AnyCancellable>()
let reference = Storage.storage()
.reference(forURL: "\(your_firebase_storage_bucket)/images/space.jpg")
// Download in memory with a maximum allowed size of 1MB (1 * 1024 * 1024 bytes)
(reference.getData(maxSize: 1 * 1024 * 1024) as AnyPublisher<Data, Error>)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): // Uh-oh, an error occurred!
}
}) { data in
// Data for "images/space.jpg" is returned
}
.store(in: &cancelBag)
// Create local filesystem URL
let localURL = URL(string: "path/to/image")!
// Download to the local filesystem
(reference.write(toFile: localURL) as AnyPublisher<URL, Error>)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): // Uh-oh, an error occurred!
}
}) { data in
// Local file URL for "images/space.jpg" is returned
}
.store(in: &cancelBag)
URL
var cancelBag = Set<AnyCancellable>()
let reference = Storage.storage()
.reference(forURL: "\(your_firebase_storage_bucket)/images/space.jpg")
// Fetch the download URL
(reference.downloadURL() as AnyPublisher<URL, Error>)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): // Uh-oh, an error occurred!
}
}) { data in
// Get the download URL for 'images/space.jpg'
}
.store(in: &cancelBag)
Metadata
var cancelBag = Set<AnyCancellable>()
let reference = Storage.storage()
.reference(forURL: "\(your_firebase_storage_bucket)/images/space.jpg")
// Create file metadata to update
let newMetadata = StorageMetadata()
// Update metadata properties
(reference.updateMetadata(newMetadata) as AnyPublisher<StorageMetadata, Error>)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): // Uh-oh, an error occurred!
}
}) { metadata in
// Updated metadata for 'images/space.jpg' is returned
}
.store(in: &cancelBag)
// Get metadata properties
(reference.getMetadata() as AnyPublisher<StorageMetadata, Error>)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): // Uh-oh, an error occurred!
}
}) { metadata in
// Metadata now contains the metadata for 'images/space.jpg'
}
.store(in: &cancelBag)
Delete
var cancelBag = Set<AnyCancellable>()
let reference = Storage.storage()
.reference(forURL: "\(your_firebase_storage_bucket)/images/space.jpg")
// Delete the file
(reference.delete() as AnyPublisher<Void, Error>)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): // Uh-oh, an error occurred!
}
}) { _ in
// File deleted successfully
}
.store(in: &cancelBag)
Create
var cancelBag = Set<AnyCancellable>()
let auth = Auth.auth()
// Create a password-based account
(auth.createUser(withEmail: "xxx@xxx.com", password: "1q2w3e4r") as AnyPublisher<AuthDataResult, Error>)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): // Uh-oh, an error occurred!
}
}) { _ in
// User signed in
}.store(in: &cancelBag)
// https://firebase.google.com/docs/auth/ios/password-auth
Sign in
var cancelBag = Set<AnyCancellable>()
let auth = Auth.auth()
// Sign in a user with an email address and password
(auth.signIn(withEmail: "xxx@xxx.com", password: "1q2w3e4r") as AnyPublisher<AuthDataResult, Error>)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): // Uh-oh, an error occurred!
}
}) { _ in
// User signed in
}.store(in: &cancelBag)
// https://firebase.google.com/docs/auth/ios/password-auth
Update email
var cancelBag = Set<AnyCancellable>()
let user = Auth.auth().currentUser
// Set a user's email address
(user.updateEmail(to: "xxx@xxx.com") as AnyPublisher<Void, Error>)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): // Uh-oh, an error occurred!
}
}) { _ in
// Completed updating Email
}.store(in: &cancelBag)
// https://firebase.google.com/docs/auth/ios/manage-users
Delete
var cancelBag = Set<AnyCancellable>()
let user = Auth.auth().currentUser
// Delete a user
(user.delete() as AnyPublisher<Void, Error>)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): // Uh-oh, an error occurred!
}
}) { _ in
// User deleted
}.store(in: &cancelBag)
// https://firebase.google.com/docs/auth/ios/manage-users
var cancelBag = Set<AnyCancellable>()
let functions = Functions.functions()
let request = functions.httpsCallable("functionName")
(request
.call(["parameter": "value"]) as AnyPublisher<HTTPSCallableResult, Error>)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: print("π finished")
case .failure(let error): print("error:\(error)")
}
}) { result in
print("response:\(result)")
}.store(in: &cancelBag)
// https://firebase.google.com/docs/functions/callable#call_the_function
Kumar Shivang, shivang.iitk@gmail.com
CombineFirebase is available under the MIT license. See the LICENSE file for more info.