我目前遇到了一个让我感到困惑的问题。我正在尝试为我的应用创建一个使用 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)
}
}
答案 0 :(得分:0)
我找到了它没有更新的原因,它与上面的设置无关。
这是我第一次使用 Resolver
进行依赖注入,我天真地认为注册一个对象只是一个实例。我的 SwiftUI 视图未更新的原因是我有两个不同的 UserDataController
实例,而设置配置文件的实例不是 SwiftUI 视图中的实例。
我在使用 .scope(.application)
注册我的 UserDataController
时使用 Resolver
函数修复了它。这个范围使它像一个单身人士一样,这正是我最初想要的。