Search code examples
iosswiftfirebaseuserdefaults

Cache server data globally and refresh views


I'm building an app that uses firebase for authentication and database functionality. Once a user signs up, a database record is stored for that user, containing some basic information like first name, last name etc.

Once a user logs in with his credentials I want to set a global variable (perhaps userDefaults?) which contains the user data for that specific user. Otherwise I have to fetch user data for every time I want to fill a label with for instance, a user's first name.

I managed to set userdefaults upon login and use this info in UIlables. But when I let users make changes to their data, of which some is important for the functioning of the app, I can update the server AND the userdefaults but the app itself doesn't update with the correct data. It keeps the old data in (for example) UIlables.

I would love to get some more insight on what the best work-flow is to manage situations like these.

When opening the app, i have a tabBarController set as rootviewcontroller. In the load of tabbarcontroller I have the following code retrieving the user data from firebase and saving it to userdefaults:

guard let uid = Auth.auth().currentUser?.uid else { return }
            Database.database().reference().child("users").child(uid).observeSingleEvent(of: .value, with: { (snapshot) in
                print(snapshot.value ?? "")


              guard let dictionary = snapshot.value as? [String: Any] else { return }
                let firstname = dictionary["First name"] as? String
                let lastname = dictionary["Last name"] as? String

                print("first name is: " + firstname!)

                UserDefaults.standard.set(firstname, forKey: "userFirstName")

                print(UserDefaults.standard.value(forKey: "userFirstName"))


                self.setupViewControllers()


            }

Then I continue on loading in all the viewcontrollers in the tabBarController:

self.setupViewControllers()

During that process the labels in those viewcontrollers get filled in with the userdefaults data.

This is an example of a label being filled in with userDefaults but not being updated upon changing of userdefaults:

    let welcomeLabel: UILabel = {
        let label = UILabel()  
        let attributedText = NSMutableAttributedString(string: "Welcome ")
        attributedText.append(NSAttributedString(string: "\(UserDefaults.standard.string(forKey: "userFirstName")!)"))
        label.attributedText = attributedText
        label.font = UIFont.systemFont(ofSize: 30, weight: .bold)
        return label
    }()

this is a function i'm using to update the first name (via a textfield filled in by the user):

    @objc func updateName() {
        guard let uid = Auth.auth().currentUser?.uid else { return }
        Database.database().reference().child("users").child(uid).updateChildValues(["First name" : updateNameField.text ?? ""])

        UserDefaults.standard.set(updateNameField.text, forKey: "userFirstName")

        print(UserDefaults.standard.value(forKey: "userFirstName"))
    }

Solution

  • So you'll have to organize things first. In a new file define constants such as below. These constant will be accessible in global scope unless private

    Constants.swift

    private let storedusername = "usname"
        private let storedName = "uname"
        private let displaypic = "udp"
        private let aboutme = "udesc"
    
        var myusername : String {
            get {
                return (UserDefaults.standard.string(forKey: storedusername)!)
            } set {
                UserDefaults.standard.set(newValue, forKey: storedusername)
            }
        }
    
        var myname : String {
            get {
                return (UserDefaults.standard.string(forKey: storedName)!)
            } set {
                UserDefaults.standard.set(newValue, forKey: storedName)
            }
        }
    
        var myProfileImage : Data {
            get {
                return (UserDefaults.standard.data(forKey: displaypic)!)
            } set {
                UserDefaults.standard.set(newValue, forKey: displaypic)
            }
        }
    
        var myAboutMe : String? {
            get {
                return (UserDefaults.standard.string(forKey: aboutme)!)
            } set {
                UserDefaults.standard.set(newValue, forKey: aboutme)
            }
        }
    

    Now the next time you want to save anything in UserDefaults, you'll just do the following anywhere throughout your code base :

    myusername = "@CVEIjk"
    

    And to retrive it, just call it :

    print(myusername)
    

    IMPORTANT NOTE --

    1. Always remember to initialize them. You can do this as the user signs up. As soon as they fill out their details and hit submit, just save them to these variables. That wouldn't cause unnecessary crash.

    2. You'll have to save them at every location you perform updates regarding these nodes in the database.

    Now, the refreshing views part. I am taking a scenario where your ProfileView.swift has the view and user goes to EditProfile.swift for updating the content.

    You initialize all your observers the place where the update will have the immediate effect. Because the view immediately after the update matters. The rest will be called through the getter of the aboutme

    ProfileView.swift

    func openEditView() {
    
            NotificationCenter.default.addObserver(self, selector: #selector(fetchUserDetails), name: Notification.Name("update"), object: nil)
    
            //setting this right before the segue will create an observer specifically and exclusively for updates. Hence you don't have to worry about the extra observers.
    
            perform(segue: With Identifier:)// Goes to the editProfile page
        }
    

    This function will be initially called in viewDidLoad(). At this time you need to make sure you have all the data, else it will produce no values. But if you are storing everything as the user signs up, you are safe.

    @objc func fetchUserDetails() {
            if uid != nil {
                if myname.count > 0 { // This will check if the variable has anything in the memory or not. Dont confuse this with [Array].count
                    self.nameLabel = myname
                }
            }
        }
    

    This function also acts an ab observer method. So when the notifications are posted they can run again.

    Now, EditProfile.swift

    In the block where you are updating the server, save the values and then create a Notification.post and put this method right before you dismiss(toViewController:)

       func updateUserCacheData(name: String, username: String, aboutme: String, ProfilePhoto: UIImage? = nil) {
            DispatchQueue.global().async {
                myname = name
                myusername = username
                myAboutMe = aboutme
                if self.newImage != nil {
                    myProfileImage = self.newImage!.jpegData(compressionQuality: 1)!
                }
                DispatchQueue.main.async {
                    NotificationCenter.default.post(name: .refreshProfileViews, object: nil)
                }
            }
        }
    
        func updateToServerAndBackToProfileView() {
            self.updateUserCacheData(name: iname!, username: iusername, aboutme: iaboutme!)
            self.dismiss(animated: true, completion: nil)
            }
        }
    

    As long as this goes back to ProfileView, your views will be instantly refreshed. You can keep an observer wherever you view will be first displayed after the dismiss. the rest will fetch updated content always. Also, don't forget to deinit your Observer in ProfileView

    //This code is in ProfileView.swift
    deinit {
            NotificationCenter.default.removeObserver(self, name: Notification.Name("update"), object: nil)
        }
    

    Also, in cases where the content might be empty, simply initialize it with empty content. For example, if user doesn't choose to add aboutme while signing up, you can just put

    `myaboutme = ""`
    

    This will create a safe environment for you and you are well set.