0

I have a user object that I am getting its values from firebase and I want to pass this object basically in all of my other view controllers. I am using storyboard and I looked up how to do this and I found out that I can override the prepare method, I wasn't successful with that as I did not know how to call the method or if it was ever called, it just didn't work. Then I found that you can just assign a vc to another view controller and pass data like that but I hit an issue:

In HomeViewController, I have this method that gets data from firebase and assign it to user:

extension HomeViewController {
public func AssignValueToUserObject() {
    guard let uid = Auth.auth().currentUser?.uid else {
        print("Could not get user id")
        return
    }
    
    Database.database().reference().child("users").child(uid).observeSingleEvent(of: .value, with: { [self] snapshot in
        if let dictionary = snapshot.value as? [String: AnyObject] {
            
            user.first_name = dictionary["first_name"] as? String
            user.last_name = dictionary["last_name"] as? String
            user.email = dictionary["email"] as? String
            user.profile_picture = dictionary["profile_picture"] as? String
        }
    }, withCancel: nil)
} // End AssignValueToUserObject Method
} // End extension

And this is what I have in HomeViewController to copy that user object to my ProfileViewController:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    title = "Home"
    checkIfUserIsLoggedIn()
    copyData()
}

func checkIfUserIsLoggedIn() {
    // Check if user is logged in
    if Auth.auth().currentUser == nil {
        // User is not logged in, send user to login screen
        let loginVC = storyboard?.instantiateViewController(identifier: "login")
        loginVC?.modalPresentationStyle = .fullScreen
        present(loginVC!, animated: false)
    }
    
    // User is logged in, fetch their info
    AssignValueToUserObject()

    } // End checkIfUserIsLoggedIn method

// Copy user from Home to Profile
func copyData() {
    let vc = storyboard?.instantiateViewController(identifier: "profile") as? ProfileViewController
    vc?.user = user
}

After debugging, I found the BIG problem, which is that the copy method gets called BEFORE the values gets assigned to user in the AssignValueToUserObject method, which to me makes absolutely no sense.

I call the assign method before the copy method so how does that work? After some research, I figured out it has something to do with completion handling but I just don't get it.

Omar
  • 41
  • 6
  • Firebase methods like `observeSingleEvent` are *asynchronous*, meaning (among other things), they do not finish executing in the current run loop that you're on. So, you can't expect `AssignValueToUserObject` to actually change your `user` object. Look into using completion handlers or callback functions to achieve what you're looking for. – jnpdx Jul 28 '21 at 15:36
  • I wouldn't have asked the question if I haven't looked into them already. Could you please show me how I would change my code so that it does what I expect it to do? – Omar Jul 28 '21 at 15:38

2 Answers2

1

As mentioned in the comments, with asynchronous functions, you can't expect a return value right away. One common way to handle this is by using a callback function or completion handler.

I'm including a very basic example of this. Note that I'm not doing any error handling right now -- you'd want to build it out to be more robust, but this at least gets the concept:

extension HomeViewController {
    public func assignValueToUserObject(completion: @escaping () -> Void) { //completion handler gets passed as an parameter
        guard let uid = Auth.auth().currentUser?.uid else {
            print("Could not get user id")
            return
        }
        
        Database.database().reference().child("users").child(uid).observeSingleEvent(of: .value, with: { [self] snapshot in
            if let dictionary = snapshot.value as? [String: AnyObject] {
                
                user.first_name = dictionary["first_name"] as? String
                user.last_name = dictionary["last_name"] as? String
                user.email = dictionary["email"] as? String
                user.profile_picture = dictionary["profile_picture"] as? String
                completion() //call once the action is done
            }
        }, withCancel: nil)
    } // End AssignValueToUserObject Method
} // End extension
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    title = "Home"
    checkIfUserIsLoggedIn()
    //don't call copy here anymore
}
func checkIfUserIsLoggedIn() {
    // Check if user is logged in
    if Auth.auth().currentUser == nil {
        // User is not logged in, send user to login screen
        let loginVC = storyboard?.instantiateViewController(identifier: "login")
        loginVC?.modalPresentationStyle = .fullScreen
        present(loginVC!, animated: false)
    }
    
    // User is logged in, fetch their info
    assignValueToUserObject(completion: {
      self.copyData() //only copy once the completion handler is run
    })
    
} // End checkIfUserIsLoggedIn method

Update, showing a way to use a singleton to monitor a user value in different view controllers:

import Combine

struct User {
    var id : UUID //whatever properties your user model has
}

class UserManager {
    @Published var user : User?
    static public var shared = UserManager()
    private init() {
        
    }
    
    func login() {
        //do your firebase call here and set `user` when done in the completion handler
        self.user = User(id: UUID())
    }
}

class HomeViewController : UIViewController {
    private var userManager = UserManager.shared
    private var cancellable : AnyCancellable?
    
    init() {
        super.init(nibName: nil, bundle: nil)
        setupUserLink() //make sure this gets called in whatever initializer is used
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupUserLink() //make sure this gets called in whatever initializer is used
    }
    
    func setupUserLink() {
        cancellable = userManager.$user.compactMap { $0 }.sink { user in
            print(user.id.uuidString) //do something with the user value -- assign it to a variable, a control, etc
        }
    }
}

class ProfileViewController : UIViewController {
    //do the same pattern as in HomeViewController, setting up the user link to be monitored
}
jnpdx
  • 45,847
  • 6
  • 64
  • 94
  • Your answer did exactly what I asked it to do. However, the data I wanted (user) still did not transfer from HomeVC to ProfileVC. I tried the segue method and that did transfer the data, but the problem with that is my view controller changes from Home to Profile which is not what I want, I want only the data to be transferred. I have been looking into this for days now. Do you know another solution? Or how I can make the segue not change view controllers and just change the data? – Omar Jul 28 '21 at 17:49
  • Segues, by definition, change the view controller. I’m afraid I don’t understand what else you are trying to do. If you’re trying to change data on an existing view controller, you have to hold a reference to it somewhere. – jnpdx Jul 28 '21 at 18:03
  • All I want is to be able to use "user" in all of my view controllers, even the ones that are not directly linked to each other via segue. If I update user in HomeVC, I want to see that update gets reflected in profileVC and so on. – Omar Jul 28 '21 at 18:36
  • Then you are going to need to come up with a global state container that gets passed by reference (or singleton, which is sometimes frowned upon as an anti pattern, which is debatable) that you can pass to each view controller. The first would take more code and be more involved to show. The second I could show you an example of. – jnpdx Jul 28 '21 at 18:40
  • I was reading documentation about singleton and about how it shouldn't be used. But it would be great if you can show me an example of how I can use it. – Omar Jul 28 '21 at 18:49
  • Okay, updated to show an example. This could also be converted to a non-singleton example by just passing a reference to `UserManager` to each new view controller whenever you do a segue. – jnpdx Jul 28 '21 at 19:22
1

Try this

func copyData() {
    let storyboard = UIStoryboard(name: "Your Storyboard Name", bundle: nil)
    let vc = storyboard.instantiateViewController(identifier: "profile") as? ProfileViewController
    vc?.user = user
    self.navigationController?.pushViewController(vc, animated: true)
}
Rahul
  • 13
  • 3
  • This is working but I do not want to push the profile view controller. As the name of the function suggests, I just want to copy the data. – Omar Jul 28 '21 at 20:13
  • Yes you have to push the profile view controller – Rahul Jul 31 '21 at 04:36
  • I do NOT want to do that, I do NOT want to view the profile view controller, I just want to copy the data. – Omar Jul 31 '21 at 17:32
  • You can use NotificationCenter to pass the data https://stackoverflow.com/questions/36910965/how-to-pass-data-using-notificationcenter-in-swift-3-0-and-nsnotificationcenter – Rahul Aug 01 '21 at 19:07