在 SwiftUI 中继续我的代码流之前,如何等待 Firebase 函数完成?

How do I wait for a Firebase function to complete before continuing my code flow in SwiftUI?

我在 class (listenToUser) 中有一个 Firebase 函数,它工作正常,但我注意到下一个代码(IF ELSE)没有等待它完成就继续。如何在继续我的代码之前等待我的功能完成?

我的主视图部分代码:

...

@EnvironmentObject var firebaseSession: FirebaseSession_VM

...
        .onAppear {
            firebaseSession.listenToUser()
            
                if firebaseSession.firebaseUser == nil {
                    showSignInView = true
                } else {
                    showSignInStep1View = true
                }
   }

我的函数:

import SwiftUI
import Combine
import FirebaseAuth

class FirebaseSession_VM: ObservableObject {
    static let instance = FirebaseSession_VM()
    
    var didChange = PassthroughSubject<FirebaseSession_VM, Never>()
    
    @Published var firebaseUser: FirebaseUser_M? {
        didSet {
            self.didChange.send(self)
        }
    }
    
    var handle: AuthStateDidChangeListenerHandle?
    
    func listenToUser () {
        // monitor authentication changes using firebase
        handle = Auth.auth().addStateDidChangeListener { (auth, user) in
            if let user = user {
                self.firebaseUser = FirebaseUser_M(
                    id: user.uid,
                    email: user.email
                )
            } else {
                self.firebaseUser = nil
            }
        }
    }
}

大多数 Firebase API 调用都是异步的,这就是为什么您需要注册状态侦听器或使用回调的原因。

两个旁注:

  1. 您不应将 ObservableObjects 实现为单例。请改用 @StateObject,以确保 SwiftUI 可以正确管理其状态。
  2. 您不再需要直接使用 PassthroughSubject。使用 @Published 属性 包装器更容易。

也就是说,这里有几个代码片段展示了如何使用 SwiftUI 实现 Email/Password 身份验证:

主视图

主视图会显示您是否已登录。如果您未登录,它将显示一个按钮,用于打开单独的登录屏幕。

import SwiftUI

struct ContentView: View {
  @StateObject var viewModel = ContentViewModel()
  
  var body: some View {
    VStack {
      Text(" Hello!")
        .font(.title3)
      
      switch viewModel.isSignedIn {
      case true:
        VStack {
          Text("You're signed in.")
          Button("Tap here to sign out") {
            viewModel.signOut()
          }
        }
      default:
        VStack {
          Text("It looks like you're not signed in.")
          Button("Tap here to sign in") {
            viewModel.signIn()
          }
        }
      }
    }
    .sheet(isPresented: $viewModel.isShowingLogInView) {
      SignInView()
    }
  }
}

主视图的视图模型侦听任何身份验证状态更改并相应地更新 isSignedIn 属性。这会驱动 ContentView 及其显示内容。

import Foundation
import Firebase

class ContentViewModel: ObservableObject {
  @Published var isSignedIn = false
  @Published var isShowingLogInView = false
  
  init() {
    // listen for auth state change and set isSignedIn property accordingly
    Auth.auth().addStateDidChangeListener { auth, user in
      if let user = user {
        print("Signed in as user \(user.uid).")
        self.isSignedIn = true
      }
      else {
        self.isSignedIn = false
      }
    }
  }
  
  /// Show the sign in screen
  func signIn() {
    isShowingLogInView = true
  }
  
  /// Sign the user out
  func signOut() {
    do {
      try Auth.auth().signOut()
    }
    catch {
      print("Error while trying to sign out: \(error)")
    }
  }
}

登录视图

SignInView 显示了一个带有按钮的简单 email/password 表单。这里要注意的有趣的事情是它监听 viewModel.isSignedIn 属性 的任何更改,并调用 dismiss 操作(它从环境中提取)。另一种选择是将回调实现为视图模型的 signIn() 方法的尾随闭包。

struct SignInView: View {
  @Environment(\.dismiss) var dismiss
  @StateObject var viewModel = SignInViewModel()
  
  var body: some View {
    VStack {
      Text("Hi!")
        .font(.largeTitle)
      Text("Please sign in.")
        .font(.title3)
      Group {
        TextField("Email", text: $viewModel.email)
          .disableAutocorrection(true)
          .autocapitalization(.none)
        SecureField("Password", text: $viewModel.password)
      }
      .padding()
      .background(Color(UIColor.systemFill))
      .cornerRadius(8.0)
      .padding(.bottom, 8)
      
      Button("Sign in") {
        viewModel.signIn()
      }
      .foregroundColor(Color(UIColor.systemGray6))
      .padding(.vertical, 16)
      .frame(minWidth: 0, maxWidth: .infinity)
      .background(Color.accentColor)
      .cornerRadius(8)
    }
    .padding()
    .onChange(of: viewModel.isSignedIn) { signedIn in
      dismiss()
    }
  }
}

SignInViewModel 有一个方法 signIn,它通过调用 Auth.auth().signIn(withEmail:password:) 执行实际的登录过程。如您所见,如果用户已通过身份验证,它会将视图模型的 isSignedIn 属性 更改为 true

import Foundation
import FirebaseAuth

class SignInViewModel: ObservableObject {
  @Published var email: String = ""
  @Published var password: String = ""
  
  @Published var isSignedIn: Bool = false
  
  func signIn() {
    Auth.auth().signIn(withEmail: email, password: password) { authDataResult, error in
      if let error = error {
        print("There was an issue when trying to sign in: \(error)")
        return
      }
      
      guard let user = authDataResult?.user else {
        print("No user")
        return
      }
      
      print("Signed in as user \(user.uid), with email: \(user.email ?? "")")
      self.isSignedIn = true
    }
  }
}

备选方案:使用 Combine

import Foundation
import FirebaseAuth
import FirebaseAuthCombineSwift

class SignInViewModel: ObservableObject {
  @Published var email: String = ""
  @Published var password: String = ""
  
  @Published var isSignedIn: Bool = false

  // ...

  func signIn() {
    Auth.auth().signIn(withEmail: email, password: password)
      .map { [=14=].user }
      .replaceError(with: nil)
      .print("User signed in")
      .map { [=14=] != nil }
      .assign(to: &$isSignedIn)
  }
}

备选方案:使用 async/await

import Foundation
import FirebaseAuth

class SignInViewModel: ObservableObject {
  @Published var email: String = ""
  @Published var password: String = ""
  
  @Published var isSignedIn: Bool = false
  

  @MainActor
  func signIn() async {
    do {
      let authDataResult = try 3 await 1 Auth.auth().signIn(withEmail: email, password: password)
      let user = authDataResult.user
    
      print("Signed in as user \(user.uid), with email: \(user.email ?? "")")
      self.isSignedIn = true
    }
    catch {
      print("There was an issue when trying to sign in: \(error)")
      self.errorMessage = error.localizedDescription
    }
  }
}

更多详情

我写了一篇关于这个的文章,其中我更详细地解释了各个技术:Calling asynchronous Firebase APIs from Swift - Callbacks, Combine, and async/await. If you'd rather watch a video, I've got you covered as well: 3 easy tips for calling async APIs