Home > Net >  SwiftUI published variable not updating view
SwiftUI published variable not updating view

Time:11-05

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:

  1. 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
        }
    }
}
  1. You need to use StateObject instead of ObservedObject 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
}
  • Related