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

时间:2021-03-12 23:49:46

标签: ios swift firebase swiftui combine

我目前遇到了一个让我感到困惑的问题。我正在尝试为我的应用创建一个使用 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 个答案:

答案 0 :(得分:0)

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

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

我在使用 .scope(.application) 注册我的 UserDataController 时使用 Resolver 函数修复了它。这个范围使它像一个单身人士一样,这正是我最初想要的。