The database is at the heart of every application, be it Android, iOS, or the Web. When it comes to iOS databases, there are so many popular options like Coredata, SQLite, and Realm.
They all have 1 disadvantage though, they are local databases and thus it’s not sufficient if we want our users to interact with each other or want to store data in the cloud.
If we want to save data in the cloud, we will need a backend, which mostly comes at a huge cost.
That’s where the Firebase database comes into the picture. I’m not going to list out all the benefits of it as it’s a well-known database in the community but you can check out official docs for more information.
Today, we will explore how we can observe items from the database in real-time using combine framework.
Initially, we will understand an example but later we will create an extension that can convert any database reference to a combine stream.
We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!
Let’s assume we want to get a user’s name from the path “users/{id}/name”. To do it, we will add a repository that wraps database interaction and hides the detail from the ViewModel.
Let’s add code to do this using combine.
public class DatabaseRepository {
private let db: DatabaseReference
public init() {
db = Database.database().reference()
}
public func getUserName(id: String) -> AnyPublisher<String?, Never> {
let subject = CurrentValueSubject<String?, Never>(nil)
let handle = db.child("users").child(id).child("name").observe(.value, with: { snapshot in
subject.send(snapshot.value as? String)
})
// Detach handle on cancellation
return subject.handleEvents(receiveCancel: {[weak self] in
self?.db.removeObserver(withHandle: handle)
}).eraseToAnyPublisher()
}
}
Here —
getUserName
, we first create a subject
that will allow callers to observe data using Combine.name
from the database and observe its value change. Whenever we get data, we pass it to subject
.Now, let’s see how this can be used in a ViewModel
public class UserViewModel: ObservableObject {
@Published var name: String = ""
private var cancelables = Set<AnyCancellable>()
private var repository = DatabaseRepository()
public init() {
observeName()
}
private func observeName() {
repository.getUserName(id: "abcd").sink { [weak self] name in
if let name, let self {
self.name = name
}
}.store(in: &cancelables)
}
}
This is very straightforward, we create an instance of DatabaseRepository
and start observing the name using sink
on a publisher.
That’s it if you want to write a one-time wrapper around Firebase Realtime Database.
However, most of the time you will need to do this multiple times in a project. Let’s see how we can write a simple extension to do this.
Basically, we will need to move our code of DatabaseRepository
in an extension function of DatabaseReference
. Let’s see how we can do that.
extension DatabaseReference {
func toAnyPublisher<T>() -> AnyPublisher<T?, Never> {
let subject = CurrentValueSubject<T?, Never>(nil)
let handle = observe(.value, with: { snapshot in
subject.send(snapshot.value as? T)
})
return subject.handleEvents(receiveCancel: {[weak self] in
self?.removeObserver(withHandle: handle)
}).eraseToAnyPublisher()
}
}
Here, we have used Generics to allow callers to get any type of data from the reference. Let’s see what changes we will need to do in our ViewModel to use this extension.
public class UserViewModel: ObservableObject {
@Published var name: String = ""
private var cancelables = Set<AnyCancellable>()
private let db: DatabaseReference
public init() {
db = Database.database().reference()
observeName()
}
private func observeName() {
db.child("users").child("user_id").child("name")
.toAnyPublisher()
.sink { [weak self] (name: String?) in
if let name, let self {
self.name = name
}
}.store(in: &cancelables)
}
}
Very easy to use right? Now you don’t have to deal with handle
and all that complexity anymore!
Let’s see if we can write a similar extension for the FirebaseStore database.
With Firestore, we will need to write an extension for DocumentReference
extension DocumentReference {
func toAnyPublisher<T: Decodable>() -> AnyPublisher<T?, Error> {
let subject = CurrentValueSubject<T?, Error>(nil)
let listener = addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
subject.send(completion: .failure(error!))
return
}
guard let data = try? document.data(as: T.self) else {
subject.send(nil)
return
}
subject.send(data)
}
return subject.handleEvents(receiveCancel: {
listener.remove()
}).eraseToAnyPublisher()
}
}
The code is very similar to Firebase Realtime Database except here we are returning the whole document as an object.
The good thing about this is, we will be able to decode this document into our swift struct directly, we just need to implement Decodable
protocol.
That’s it for today, I hope you learned something!
This is just the basics of the Firebase database but is very useful as mostly we will use this for real-time changes in our database. Add/Update/Delete calls are mostly one-time calls, so no need to use Combine for those operations.