Use Firestore and Firebase Realtime Database with Combine

Observe items from the Firebase databases in real-time using combine framework.
Nov 24 2022 · 4 min read

Background

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 get started!

Firebase realtime database

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 —

  1. We first create a reference of the database in repository initialization.
  2. In getUserName , we first create a subject that will allow callers to observe data using Combine.
  3. Next, we get the reference name from the database and observe its value change. Whenever we get data, we pass it to subject .
  4. At last, we add a listener in the subject to get notified when the publisher is canceled, this will make sure we don’t keep observing the database once the caller is done with it.

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.

Combine Extension for Firebase Realtime Database

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.

Combine Extension for Firestore 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.

Conclusion

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.

Related Useful Article


jimmy image
Jimmy Sanghani
Jimmy Sanghani is a tech nomad and cofounder at Canopas helping businesses use new age leverage - Code and Media - to grow their revenue exponentially. With over a decade of experience in both web and mobile app development, he has helped 100+ clients transform their visions into impactful digital solutions. With his team, he's helping clients navigate the digital landscape and achieve their objectives, one successful project at a time.


jimmy image
Jimmy Sanghani
Jimmy Sanghani is a tech nomad and cofounder at Canopas helping businesses use new age leverage - Code and Media - to grow their revenue exponentially. With over a decade of experience in both web and mobile app development, he has helped 100+ clients transform their visions into impactful digital solutions. With his team, he's helping clients navigate the digital landscape and achieve their objectives, one successful project at a time.

contact-footer
Say Hello!
footer
Subscribe Here!
Follow us on
2024 Canopas Software LLP. All rights reserved.