I just want an example of how Swifts new async/await
concurrency features can be used to rewrite a common Firestore listener method like the one below.
func listen(for user: User, completion: (Result<CurrentUser, Error>) -> Void) {
db.collection("Users")
.document(user.uid)
.addSnapshotListener(includeMetadataChanges: true) { document, error in
if let error = error {
completion(.failure(.networkError)
} else {
guard let document = document else {
completion(.failure(.operationFailure)
return
}
do {
guard let profile = try document.data(as: Profile.self, with: .estimate) else {
completion(.failure(.operationFailure)
return
}
completion(CurrentUser(user, profile: profile))
} catch {
completion(.failure(.operationFailure)
}
}
}
}
CodePudding user response:
OK, like I said in my comment I don't think this is the right use case for Async/Await. Async/Await is more suited to asynchronous functions where you would receive back a single response. For instance a REST api that returns some value.
Incidentally, the Firestore function .getDocument()
now has async/await alternatives.
However, the addSnapshotListener
is something that will return multiple values over time and call the callback function over and over again.
What we can do with this, though, is to turn it into a Combine Publisher.
Here I've created a small FirestoreSubscription
struct that you can use to subscribe to a document path...
import Combine
import FirebaseFirestore
import FirebaseFirestoreSwift
struct FirestoreSubscription {
static func subscribe(id: AnyHashable, docPath: String) -> AnyPublisher<DocumentSnapshot, Never> {
let subject = PassthroughSubject<DocumentSnapshot, Never>()
let docRef = Firestore.firestore().document(docPath)
let listener = docRef.addSnapshotListener { snapshot, _ in
if let snapshot = snapshot {
subject.send(snapshot)
}
}
listeners[id] = Listener(document: docRef, listener: listener, subject: subject)
return subject.eraseToAnyPublisher()
}
static func cancel(id: AnyHashable) {
listeners[id]?.listener.remove()
listeners[id]?.subject.send(completion: .finished)
listeners[id] = nil
}
}
private var listeners: [AnyHashable: Listener] = [:]
private struct Listener {
let document: DocumentReference
let listener: ListenerRegistration
let subject: PassthroughSubject<DocumentSnapshot, Never>
}
The subscribe
function returns an AnyPublisher<DocumentSnapshot, Never>
(so currently it doesn't handle any errors.
I also created a FirestoreDecoder
that will decode DocumentSnapshot
into my own Codable
types...
import Firebase
struct FirestoreDecoder {
static func decode<T>(_ type: T.Type) -> (DocumentSnapshot) -> T? where T: Decodable {
{ snapshot in
try? snapshot.data(as: type)
}
}
}
I created a really simple Firestore document...
And a struct that we will decode from that document...
struct LabelDoc: Codable {
let value: String?
}
Now in my ViewController
I can subscribe to that document path and decode and set it onto a label...
import UIKit
import Combine
class ViewController: UIViewController {
@IBOutlet weak var label: UILabel!
var cancellables: Set<AnyCancellable> = []
struct SubscriptionID: Hashable {}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
FirestoreSubscription.subscribe(id: SubscriptionID(), docPath: "labels/title")
.compactMap(FirestoreDecoder.decode(LabelDoc.self))
.receive(on: DispatchQueue.main)
.map(\LabelDoc.value)
.assign(to: \.text, on: label)
.store(in: &cancellables)
}
}
This is just a quick example project so there may be better ways of doing this but now I can update the value in Firestore and it will immediately update on the screen