Context: I am trying to use firebase authentication and firestore to get the user's data. The problem I am running into is that the views are presented before the data is completely fetched and that obviously causes the app to crash. That being said, I am utilizing the firebase authentication listener in my app delegate to ensure the user is authenticated before fetching the users' data (which is also done in the app delegate as shown below)
App delegate snippet
class AppDelegate: NSObject, UIApplicationDelegate {
var handle: AuthStateDidChangeListenerHandle?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
self.handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if (user != nil){
print("UserAuthentication User authenticated in delegate")
DatabaseDelegate().getUserInfo(UID: user!.uid, withCompletionHandler: {
print("got user data")
})
} else {
print(" UserAuthentication User not authenticated in delegate")
try! Auth.auth().signOut()
}
}
return true
}
This is the database code I am querying and want to listen for when the data is finished loading:
class DatabaseDelegate: ObservableObject {
@Published var userDataLoaded = Bool()
func getUserInfo(UID: String, withCompletionHandler completionHandler: @escaping () -> Void) {
database.collection("Users").document(UID).getDocument { (document, error) in
if let document = document, document.exists {
let data = document.data()!
guard let UID = data["UUID"] as? String else { return }
guard let Name = data["Name"] as? String else { return }
guard let PhoneNumber = data["PhoneNumber"] as? String else { return }
guard let StripeID = data["StripeID"] as? String else { return }
self.userDataLoaded = true
UserData.append(User(UID: UID, Name: Name, PhoneNumber: PhoneNumber, StripeID: StripeID, PurchasedContent: ["TEMP": true]))
completionHandler()
}
}
}
}
And this is the SwiftUI view I want to update based on the userDataLoaded above:
struct MainViewDelegate: View {
//MARK: VARIABLES
@State var showAnimation = true
@State var locationHandler = LocationHandler()
@ObservedObject var databaseDelegate = DatabaseDelegate()
init(){
//Check and enable user location
locationHandler.requestAuthorisation()
}
var body: some View {
VStack {
//Check if data has finished loading, if not, show loading. Listen for changes when the data is finished loading and then present the tab view when it is.
switch databaseDelegate.userDataLoaded {
case true:
TabView {
HomeView()
.tabItem {
Label("Home", systemImage: "house")
}
CheckoutView()
.tabItem {
Label("Services", systemImage: "bag")
}
SettingsView()
.tabItem {
Label("Settings", systemImage: "gearshape")
}
}
case false:
Text("Loading data")
}
}
}
}
Thank you in advanced. I am new to swiftui (transitioning from uikit) and I've spent too much time trying to solve this silly issue
CodePudding user response:
- You're using two different instances of
DatabaseDelegate
, one in the AppDelegate and one in the MainViewDelegate. The boolean is only updated in app delegate's instance.
Move your auth listener into your DatabaseDelegate.
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
}
class DatabaseDelegate: ObservableObject {
@Published var userDataLoaded = false
private var handle: AuthStateDidChangeListenerHandle?
init() {
self.handle = Auth.auth().addStateDidChangeListener { (auth, user) in
// .. etc
self.getUserInfo(...)
}
}
private func getUserInfo(UID: String, withCompletionHandler completionHandler: @escaping () -> Void) {
database.collection("Users").document(UID).getDocument { (document, error) in
// .. etc
self.userDataLoaded = true
}
}
}
- You need to use
StateObject
instead ofObservedObject
since you are initializing it internally on the view, instead of injecting it from an external source.
struct MainViewDelegate: View {
@StateObject private var databaseDelegate = DatabaseDelegate()
}
If you want to use ObservedObject, you can create it externally and inject into the view like so:
var databaseDelegate = DatabaseDelegate()
MainViewDelegate(databaseDelegate: databaseDelegate)
struct MainViewDelegate: View {
@ObservedObject var databaseDelegate: DatabaseDelegate
}