在 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 分析中设置用户属性?
如何在 Flutter Web 项目中设置 Firebase 功能