在SwiftUI中,当用户通过Firebase使用Google登录后,如何重新呈现我的视图?

3
我对iOS编程和SwiftUI还比较新手。我遇到了一个问题,尝试在我的应用程序中集成Firebase身份验证来管理用户。
现在登录和注销基本上都能工作,但问题是,在登录后,我的视图(有条件地呈现Google登录按钮或内容列表)不会重新渲染,因此即使我已登录,仍然看到登录按钮。
我设置了一个可观察的对象来保存我的身份验证状态,但不幸的是,它不会自动重新加载当前用户。因此,我设置了一个手动重新加载用户的函数,希望在登录时触发。这对于注销按钮有效,但是登录完成后在AppDelegate中,由于某种原因我无法访问reloadUser()函数。
我确信有更好的方法来解决这个问题,并感激任何帮助!
环境:
final class UserData: ObservableObject {
    @Published var showFavoritesOnly = false
    @Published var qrTags = qrTagData
    @Published var user: User? = Auth.auth().currentUser

    func reloadUser() -> Void {
        self.user = Auth.auth().currentUser
    }
}

我想渲染的视图:

struct MyQuaggsList: View {
        @EnvironmentObject private var userData: UserData

        var body: some View {
            Group {
                if getLogInState() != nil {
                    VStack {
                        NavigationView {
                            List {
                                Toggle(isOn: $userData.showFavoritesOnly) {
                                    Text("Show Favorites Only")
                                }

                                ForEach(userData.qrTags) { qrTag in
                                    if !self.userData.showFavoritesOnly || qrTag.isFavorite {
                                        NavigationLink(
                                            destination: QuagDetail(qrTag: qrTag)
                                                .environmentObject(self.userData)
                                        ) {
                                            QuaggRow(qrTag: qrTag)
                                        }
                                    }
                                }
                            }
                            .navigationBarTitle(Text("My Quaggs"))
                        }
                        SignOutButton()
                    }
                } else {
                    SignInView()
                }
            }.onAppear(perform: {self.userData.reloadUser()})
        }

        func getLogInState() -> User? {
            return Auth.auth().currentUser
        }
    }

此外,请注意有一个.onAppear()函数,不幸的是它只会在初始出现时触发,而不会在用户登录后重新出现视图时触发。

非常感谢您提前的帮助!这一直让我很沮丧。

2个回答

3

firebase 和 swiftUI 的结合起初可能有些棘手,但你会发现相同的模式在每个项目中都有所使用,不用担心。
只需按照我的步骤进行定制即可,以下是我们的策略。

- 这可能是一个较长的答案,但我希望它成为所有 Firebase-SwiftUI 用户在 Stack Overflow 中管理的参考资料。

  1. 创建一个 SessionStore 类,提供 BindableObject 并监听用户身份验证并处理 Auth 和 CRUD 方法。
  2. 为项目创建一个模型(您已经完成了)
  3. 在 SessionStore 类中添加 Auth 方法。
  4. 监听变化并将所有内容放在一起。

让我们从 SessionStore 类开始:
import SwiftUI
import Firebase
import Combine

class SessionStore : BindableObject {
    var didChange = PassthroughSubject<SessionStore, Never>()
    var session: User? { didSet { self.didChange.send(self) }}
    var handle: AuthStateDidChangeListenerHandle?

    func listen () {
        // monitor authentication changes using firebase
        handle = Auth.auth().addStateDidChangeListener { (auth, user) in
            if let user = user {
                // if we have a user, create a new user model
                print("Got user: \(user)")
                self.session = User(
                    uid: user.uid,
                    displayName: user.displayName
                )
            } else {
                // if we don't have a user, set our session to nil
                self.session = nil
            }
        }
    }

    // additional methods (sign up, sign in) will go here
}

注意我们声明了会话属性是一个可选的User类型,而此类型我们尚未定义。让我们来快速定义一下:

class User {
    var uid: String
    var email: String?
    var displayName: String?

    init(uid: String, displayName: String?, email: String?) {
        self.uid = uid
        self.email = email
        self.displayName = displayName
    }

}

现在,添加注册 (signUp)登录 (signIn)退出 (signOut)方法。

class SessionStore : BindableObject {

    // prev code...

    func signUp(
        email: String,
        password: String,
        handler: @escaping AuthDataResultCallback
        ) {
        Auth.auth().createUser(withEmail: email, password: password, completion: handler)
    }

    func signIn(
        email: String,
        password: String,
        handler: @escaping AuthDataResultCallback
        ) {
        Auth.auth().signIn(withEmail: email, password: password, completion: handler)
    }

    func signOut () -> Bool {
        do {
            try Auth.auth().signOut()
            self.session = nil
            return true
        } catch {
            return false
        }
    }
}

最后,我们需要一种方法来 停止监听我们的身份验证 更改处理程序。
class SessionStore : BindableObject {

    // prev code...

    func unbind () {
        if let handle = handle {
            Auth.auth().removeStateDidChangeListener(handle)
        }
    }
}

最后,让我们来查看一下我们的内容:

import SwiftUI

struct ContentView : View {

  @EnvironmentObject var session: SessionStore
  var body: some View {
    Group { 
     if (session.session != nil) {  
      Text("Hello user!") 
     } else {
        Text("Our authentication screen goes here...")   
    }  
   }
  }
}

1
出色的逐步指南 :) 很棒的答案。 - davidev
谢谢你的回答,我会在今天晚些时候尝试一下! - deeliciouscode
2
这个答案可以通过将 BindableObject 替换为 ObservableObject 并使用户 @Published 来改进 - 这将允许您获得自动状态处理。此外,无需实现自己的用户对象 - Firebase 的 User 类已经具有您需要的大部分属性。请参阅 https://dev59.com/ebjna4cB1Zd3GeqP8VAN#60205753 以获取完整的实现,并了解 Firebase 如何处理用户身份验证的一些背景信息。 - Peter Friese
@PeterFriese 在我回答之前我甚至没有看到你的答案,但这也是一个好的迹象。另外感谢您提供的参考! - deeliciouscode

2

@Ghazi Tozri再次感谢您的回答,虽然它不完全是我想要的,但它让我朝着正确的方向前进。

我想在这里分享一下我最终做的事情,以便如果有人不想使用电子邮件/密码登录,而是使用Google登录,他们也可以从中受益。

我使用了Combine框架+@Publisher语法,使它更易读,同时我也不需要签入和签出方法,因为Google提供了它们。

用于Google登录的SwiftUI按钮将如下所示:

struct GoogleSignIn : UIViewRepresentable {
    @EnvironmentObject private var userData: SessionStore

    func makeUIView(context: UIViewRepresentableContext<GoogleSignIn>) -> GIDSignInButton {
        let button = GIDSignInButton()
        button.colorScheme = .dark
        GIDSignIn.sharedInstance()?.presentingViewController = UIApplication.shared.windows.last?.rootViewController

        //If you want to restore a session
        //GIDSignIn.sharedInstance()?.restorePreviousSignIn()
        return button
    }

    func updateUIView(_ uiView: GIDSignInButton, context: UIViewRepresentableContext<GoogleSignIn>) {
    }
}

使用SessionStore的代码如下:

import SwiftUI
import Combine
import Firebase
import GoogleSignIn

final class SessionStore: ObservableObject {
    @Published var showFavoritesOnly = false
    @Published var qrTags = qrTagData
    @Published var session: User?
    var handle: AuthStateDidChangeListenerHandle?

    func listen() {
        handle =  Auth.auth().addStateDidChangeListener { (auth, user) in
            if let user = user {
                self.session = user
            } else {
                self.session = nil
            }
        }
    }
}

在检查身份验证状态的视图中,我使用.onAppear()函数,如下所示:

struct UserProfile: View {
    @EnvironmentObject private var session: SessionStore

    var body: some View {
        VStack {
            if session.session != nil {
                SignOutButton()
            } else {
                SignInView()
            }
        }.onAppear(perform: {self.session.listen()}) 
    }
}

要退出登录,可以使用以下功能(来自 Firebase 文档):
func signOut() -> Void {
        let firebaseAuth = Auth.auth()
        do {
            try firebaseAuth.signOut()
        }
        catch let signOutError as NSError {
            print ("Error signing out: %@", signOutError)
        }
    }

希望这也能对其他人有所帮助。

1
你可能想把 googleSignIn 改成 GoogleSignIn(类和结构体的名称应该用大写字母 - 参见各种 Swift 风格指南,如 https://google.github.io/swift/)。使用 ObservableObject 而不是 BindableObject 是实现状态处理的正确方式 - 在 SwiftUI 最初推出之后,Apple 相对较快地引入了它。 - Peter Friese
你是完全正确的,我也会在我的回答中进行编辑! - deeliciouscode

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接