xTwisteDx
xTwisteDx

Reputation: 2472

Why do I have to set @ObservedObject reference from my singleton to update data?

This code is a simple view. I have a singleton called UserProfile which is called early on in the app lifecycle. When a user registers, logs in, or is logged in, the details for that User profile are set. Then this view is eventually presented to the user. The code in question is @ObservedObject var profile = UserProfile.sharedProfile. if I remove that line of code, the Text(...) views won't display anything at all. If I have that line of code, they work just fine. The singleton is being initialized at app launch when it reaches my base view.

How do I do this a better way so that I can always just access the UserProfile.sharedProfile?

struct BaseHomeScreenView: View {
    @ObservedObject var baseHomeScreenViewModel = BaseHomeScreenViewModel()
    //This object is not being used anywhere but it does affect the view below.
    @ObservedObject var profile = UserProfile.sharedProfile
    
    var body: some View {
        ZStack {
            VStack {
                Text("Hello")
                Text(UserProfile.sharedProfile.firstName)
                Text(UserProfile.sharedProfile.lastName)
                Text(UserProfile.sharedProfile.email)
                Image(uiImage: UserProfile.sharedProfile.profileImage)
            }
        }
    }
}

First View of My App

class BaseViewModel: ObservableObject {
    @Published var userFlow: UserFlow = .login
    
    init(){
        if Auth.auth().currentUser != nil {
            userFlow = .home
            UserProfile.sharedProfile.fetchProfileInfo()
        } else {
            userFlow = .login
        }
    }
    
    enum UserFlow {
        case onboarding, login, home
    }
}

Singleton/UserProfile

class UserProfile: ObservableObject {
    static var sharedProfile = UserProfile()
    
    @Published var profileImage: UIImage
    @Published var firstName: String
    @Published var lastName: String
    @Published var email: String
    
    init() {
        self.profileImage = UIImage()
        self.firstName = ""
        self.lastName = ""
        self.email = ""
    }
        
    func setUserData(profileImageURL: String, firstName: String, lastName: String, email: String) {
        fetchImageFromURL(url: URL(string: profileImageURL)!)
        self.firstName = firstName
        self.lastName = lastName
        self.email = email
    }
    
    func fetchProfileInfo() {
        guard let uid = Auth.auth().currentUser?.uid else {
            print("Error: UserProfile.fetchProfileInfo() - Failed to fetch user id.")
            return
        }
        let db = Firestore.firestore()
        let fsUserProfile = db.collection("users").document(uid)
        
        fsUserProfile.getDocument { (snapshot, err) in
            if err != nil { return }
            self.fetchImageFromURL(url: URL(string: snapshot?.get("profile_image_url") as? String ?? "")!)
            self.firstName = snapshot?.get("first_name") as? String ?? ""
            self.lastName = snapshot?.get("last_name") as? String ?? ""
            self.email = snapshot?.get("email") as? String ?? ""
        }
    }
    
    func fetchImageFromURL(url: URL) {
        getData(from: url) { data, response, error in
                guard let data = data, error == nil else {
                    if let error = error {
                        print("Error: UserProfile.fetchImageFromURL() - Failed to fetch image from URL. | \(error.localizedDescription)")
                    } else {
                        print("Error: UserProfile.fetchImageFromURL() - Failed to get data while fetching image.")
                    }
                    return
                }
                DispatchQueue.main.async() {
                    self.profileImage =  UIImage(data: data)!
                }
            }
    }
    
    private func getData(from url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> ()) {
        URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
    }
}

Upvotes: 1

Views: 197

Answers (1)

New Dev
New Dev

Reputation: 49590

SwiftUI needs to know when something has changed in order to re-compute the body of the view that depends on the change.

With objects, it uses an @ObservedObject property wrapper (or @StateObject, or @EnvironmentObject), and the object has to conform to ObservableObject. When that object signals a change - either by updating its @Published property or by calling objectWillChange.send() directly - the SwiftUI knows to update the view.


So, in your example, when @ObservedObject var profile property "changes", body is recomputed, and by virtue of recomputing, reads UserProfile.sharedProfile and its properties.

Upvotes: 2

Related Questions