如何将 Swift 的新 Async/Await 功能与 Firestore 侦听器结合使用

oty*_*tin 9 async-await firebase swift google-cloud-firestore

我只是想要一个示例,说明如何async/await使用 Swift 新的并发功能来重写常见的 Firestore 侦听器方法,如下所示。

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)
                    }
                }
            }
    }
Run Code Online (Sandbox Code Playgroud)

Fog*_*ter 9

好的,就像我在评论中所说的那样,我认为这不是异步/等待的正确用例。Async/Await 更适合异步函数,您将在其中收到单个响应。例如返回某个值的 REST api。

顺便说一句,Firestore 函数.getDocument()现在有 async/await 替代方案。

然而,addSnapshotListener随着时间的推移,它会返回多个值并一遍又一遍地调用回调函数。

不过,我们可以用它来做的就是将其变成一个组合发布器。

在这里,我创建了一个小FirestoreSubscription结构,您可以使用它来订阅文档路径......

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>
}
Run Code Online (Sandbox Code Playgroud)

subscribe函数返回一个AnyPublisher<DocumentSnapshot, Never>(所以目前它不处理任何错误。

我还创建了一个FirestoreDecoder可以解码DocumentSnapshot为我自己的Codable类型的...

import Firebase

struct FirestoreDecoder {
    static func decode<T>(_ type: T.Type) -> (DocumentSnapshot) -> T? where T: Decodable {
        { snapshot in
            try? snapshot.data(as: type)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我创建了一个非常简单的 Firestore 文档...

在此输入图像描述

我们将从该文档中解码一个结构......

struct LabelDoc: Codable {
  let value: String?
}
Run Code Online (Sandbox Code Playgroud)

现在,ViewController我可以订阅该文档路径并解码并将其设置到标签上......

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)
  }
}
Run Code Online (Sandbox Code Playgroud)

这只是一个快速示例项目,因此可能有更好的方法来执行此操作,但现在我可以更新 Firestore 中的值,它将立即在屏幕上更新

您甚至可以将该订阅包装到一个可以在多个地方使用的函数中。

  • @otymartin 如果它解决了您的问题,请考虑[接受答案](https://stackoverflow.com/help/accepted-answer) (2认同)