在 Firebase 完成块中设置 @Published var 不更新 SwiftUI 视图

Posted

技术标签:

【中文标题】在 Firebase 完成块中设置 @Published var 不更新 SwiftUI 视图【英文标题】:Setting @Published var in Firebase Completion Block Not Updating SwiftUI View 【发布时间】:2021-03-12 23:49:46 【问题描述】:

我目前遇到了一个令我困惑的问题。我正在尝试为我的应用创建一个使用 Firebase 的用户,这是一个 SwiftUI 应用。我有一个 UserDataController 持有 @Published var profile: Profile? 变量。

我注意到的是,在 Firebase 中创建 Profile 后,我得到了带有数据的回调并将其解码到我的模型中。然后我在我发布的属性上设置解码模型。但是,当我这样做时,SwiftUI 视图并没有像我预期的那样发生变化。

在使用测试数据引入 Firebase 之前,我已经测试了此功能。当我使用测试数据设置发布的属性时,我确实看到 SwiftUI 视图相应地更新。即使我更新 DispatchQueue.main.asyncAfter 块中的已发布属性以模拟网络请求,我也会看到这种情况。

我是否做错了什么导致 SwiftUI 无法更新?

另请注意,我使用Resolver 进行UserDataController 注射。 @InjectedObject 抓取 @ObservedObject 以用于 SwiftUI 视图。

这是我的代码:

App.swift

import Resolver
import SwiftUI

@main
struct MyApp: App 

    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    @InjectedObject var userController: UserDataController

    init() 
          // This is the method that calls `DispatchQueue.main.asyncAfter` which causes the
          // view to update correctly.
//        userController.authenticate()
    

    var body: some Scene 
        WindowGroup 
            // This is where SwiftUI should be updating to show the profile instead of 
            // the LandingView since we have been logged in.
            if let profile = userController.profile 
                ProfileView()
                    .environmentObject(ProfileViewModel(profile: profile))
             else 
                // This is where the login form is
                LandingView()
            
        
    

UserDataController.swift

import Firebase
import FirebaseAuth
import FirebaseFirestoreSwift
import Foundation

// This AuthError also will not show as an alert when set from a completion block
enum AuthError: Error, Identifiable 

    var id: AuthError  self 

    case noUser
    case emailExists
    case couldNotSignOut
    case generic


final class UserDataController: ObservableObject 

    @Published var profile: Profile? 
        didSet 
            print("Profile: \(profile)")
        
    
    @Published var user: User?

    @Published var authError: AuthError?

    private lazy var db = Firestore.firestore()

    private var authStateListener: AuthStateDidChangeListenerHandle?
    private var profileListener: ListenerRegistration?

    // MARK: Auth

    func authenticate() 
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) 
            self.profile = TestData.amyAlmond
        
    

    func login(email: String, password: String) 
        applyStateListener()

        Auth.auth().signIn(withEmail: email, password: password)  [weak self] result, error in
            if let error = error 
                self?.authError = .generic
             else if let user = result?.user 
                self?.addSnapshotListener(for: user)
             else 
                self?.authError = .noUser
            
        
    

    func signUp(email: String, password: String, firstName: String, lastName: String) 
        applyStateListener()

        Auth.auth().createUser(withEmail: email, password: password)  [weak self] result, error in
            if let error = error 
                self?.authError = .generic
             else if let user = result?.user 
                self?.addSnapshotListener(for: user)
                self?.createProfile(for: user, firstName: firstName, lastName: lastName)
             else 
                self?.authError = .noUser
            
        
    


// MARK: - Private

private extension UserDataController 

    func applyStateListener() 
        guard authStateListener == nil else  return 

        authStateListener = Auth.auth().addStateDidChangeListener  [weak self] auth, user in
            guard let self = self else  return 

            if let user = auth.currentUser 
                self.user = user
             else 
                self.user = nil
                self.profile = nil

                self.profileListener?.remove()
                self.profileListener = nil

                if let stateListener = self.authStateListener 
                    Auth.auth().removeStateDidChangeListener(stateListener)
                    self.authStateListener = nil
                
            
        
    

    func addSnapshotListener(for user: User) 
        guard profileListener == nil else  return 

        profileListener = db.collection("profiles").document(user.uid).addSnapshotListener  [weak self] snapshot, error in
            guard let self = self else  return 
            guard let snapshot = snapshot else  return 

            do 
                // Setting the profile here does not change the SwiftUI view
                // These blocks happen on the main thread as well, so wrapping this
                // in a `DispatchQueue.main.async` does nothing.
                self.profile = try snapshot.data(as: Profile.self)
             catch 
                print("Error Decoding Profile: \(error)")
            
        
    

    func createProfile(for user: User, firstName: String, lastName: String) 
        let profile = Profile(uid: user.uid, firstName: firstName, lastName: lastName, farms: [], preferredFarmId: nil)

        do 
            try db.collection("profiles").document(user.uid).setData(from: profile)
         catch 
            print(error)
        
    

LandingView.swift

import Resolver
import SwiftUI

struct LandingView: View 

    @InjectedObject private var userController: UserDataController

    var body: some View 
        VStack(spacing: 10) 
            LightText("Title")
                .font(.largeTitle)

            Spacer()

            AuthenticationView()

            Spacer()
        
        .frame(maxWidth: .infinity)
        .padding()
        .alert(item: $userController.authError)  error -> Alert in
            Alert(title: Text("Oh Boy"), message: Text("Something went wrong"), dismissButton: .cancel())
        
    

AuthenticationView.swift

import SwiftUI

struct AuthenticationView: View 

    @StateObject private var viewModel = AuthenticationViewModel()

    var body: some View 
        VStack 
            VStack 
                Group 
                    switch viewModel.mode 
                    case .login:
                        loginForm
                    case .signUp:
                        signUpForm
                    
                
                .textFieldStyle(RoundedBorderTextFieldStyle())
            
            .padding()
            .background(
                RoundedRectangle(cornerRadius: 20)
                    .foregroundColor(Color.gray)
            )

            Button(action: viewModel.switchMode) 
                Text(viewModel.switchModeTitle)
            
            .padding(.bottom, 10)

            Button(action: viewModel.submitAction) 
                Text(viewModel.submitButtonTitle)
            
            .disabled(!viewModel.isValid)
        
        .padding()
    


private extension AuthenticationView 

    @ViewBuilder
    var loginForm: some View 
        TextField("Email Address", text: $viewModel.emailAddress)
        TextField("Password", text: $viewModel.password)
    

    @ViewBuilder
    var signUpForm: some View 
        TextField("First Name", text: $viewModel.firstName)
        TextField("Last Name", text: $viewModel.lastName)
        TextField("Email Address", text: $viewModel.emailAddress)
        TextField("Password", text: $viewModel.password)
        TextField("Confirm Password", text: $viewModel.confirmPassword)
    

AuthenticationViewModel.swift

import Foundation
import Resolver

final class AuthenticationViewModel: ObservableObject 

    @Injected private var userController: UserDataController

    enum Mode 
        case login, signUp
    

    @Published var firstName: String = ""
    @Published var lastName: String = ""
    @Published var emailAddress: String = ""
    @Published var password: String = ""
    @Published var confirmPassword: String = ""

    @Published var mode: Mode = .login


extension AuthenticationViewModel 

    var isValid: Bool 
        switch mode 
        case .login:
            return !emailAddress.isEmpty && isPasswordValid
        case .signUp:
            return !firstName.isEmpty
                && !lastName.isEmpty
                && !emailAddress.isEmpty
                && isPasswordValid
                && !confirmPassword.isEmpty
                && password == confirmPassword
        
    

    var submitButtonTitle: String 
        switch mode 
        case .login:
            return "Login"
        case .signUp:
            return "Create Account"
        
    

    var switchModeTitle: String 
        switch mode 
        case .login:
            return "Create a New Account"
        case .signUp:
            return "Login"
        
    

    func switchMode() 
        if mode == .login 
            mode = .signUp
         else 
            mode = .login
        
    

    func submitAction() 
        switch mode 
        case .login:
            loginUser()
        case .signUp:
            createUser()
        
    


private extension AuthenticationViewModel 

    var isPasswordValid: Bool 
        !password.isEmpty && password.count > 8
    

    func loginUser() 
        userController.login(email: emailAddress, password: password)
    

    func createUser() 
        userController.signUp(email: emailAddress, password: password, firstName: firstName, lastName: lastName)
    

【问题讨论】:

【参考方案1】:

我找到了它没有更新的原因,它与上面的设置无关。

这是我第一次使用Resolver 进行依赖注入,我天真地认为注册一个对象会产生一个实例。我的 SwiftUI 视图没有更新的原因是因为我有两个不同的 UserDataController 实例,而设置配置文件的那个不是 SwiftUI 视图中的那个。

我在用Resolver 注册我的UserDataController 时使用.scope(.application) 函数修复了它。这个作用域使它表现得像一个单例,这就是我最初想要的。

【讨论】:

以上是关于在 Firebase 完成块中设置 @Published var 不更新 SwiftUI 视图的主要内容,如果未能解决你的问题,请参考以下文章

在 Vue.js + Firebase 中设置项目的更好方法是啥? [复制]

如何在 Flutter Web 的 Firebase 分析中设置用户属性?

在 xcode 中设置 Firebase 的问题

如何在 Flutter Web 项目中设置 Firebase 功能

无法在 android studio 中设置 firebase

在 Firebase 侦听器中设置 Singleton 属性值