1

Goal

Create a function that listens to changes in Firestore and publishes the result or the error

The code

func observe<T: Codable>(document: String, inCollection collection: String) -> AnyPublisher<T, Error> {
        let docRef = self.db.collection(collection).document(document)
        return Future<T, Error> { promise in
            let docRef = self.db.collection(collection).document(document)
            let listener = docRef.addSnapshotListener { (snapshot, error) in
                guard let object = T(dictionary: snapshot?.data()), error == nil else {
                    promise(.failure(error ?? CRUDServiceError.encodingError))
                    return
                }
                promise(.success(object))
            }
            
            // Cancel the listener when the publisher is deallocated
            let cancellable = AnyCancellable {
                listener.remove()
            }
        }.eraseToAnyPublisher()
    }
  1. Future by define produces only a single value, not suitable for subscribing. PassThroughSubject inside the function failed also.
  2. Error leads to publisher completion. We want to keep listening to changes even after the error is received, I found multiple approaches to achieve this, but they all require specific code on subscribing. I want to handle this problem one time inside the observe function. You can read some solutions here
jnpdx
  • 45,847
  • 6
  • 64
  • 94

1 Answers1

1

Check out this gist: https://gist.github.com/IanKeen/934258d2d5193160391e14d25e54b084

With the above gist you can then do:

func observe<T: Codable>(document: String, inCollection collection: String) -> AnyPublisher<T, Error> {
    let docRef = self.db.collection(collection).document(document)
    return AnyPublisher { subscriber in
        let docRef = self.db.collection(collection).document(document)
        let listener = docRef.addSnapshotListener { (snapshot, error) in
            guard let object = T(dictionary: snapshot?.data()), error == nil else {
                subscriber.send(completion: .failure(error ?? CRUDServiceError.encodingError))
                return
            }
            subscriber.send(object)
        }

        return AnyCancellable { listener.remove() }
    }
}

As for the error... A Publisher cannot emit any more values after it emits an error. This is a fundamental part of the contract. Then best you can do is convert the error into some other type and emit as a next event instead of a completion event.

Something like this would do it:

func observe<T: Codable>(document: String, inCollection collection: String) -> AnyPublisher<Result<T, Error>, Never> {
    let docRef = self.db.collection(collection).document(document)
    return AnyPublisher { subscriber in
        let docRef = self.db.collection(collection).document(document)
        let listener = docRef.addSnapshotListener { (snapshot, error) in
            guard let object = T(dictionary: snapshot?.data()), error == nil else {
                subscriber.send(.failure(error ?? CRUDServiceError.encodingError))
                return
            }
            subscriber.send(.success(object))
        }

        return AnyCancellable { listener.remove() }
    }
}

But I suspect that once Firestore send an error to the callback, it too will stop calling your callback with new snapshots... So I don't think this is actually useful.

Daniel T.
  • 32,821
  • 6
  • 50
  • 72