Data is loaded from Firebase asynchronously, since it may take some time to come down from the server/network. Your main application code continues to run while the data is being loaded. Then once the data is available, your completion handler is called.
For this reason, any code that requires data from the database, must be inside the completion handler, or called from there. Putting it anywhere else makes it uncertain that the data will have loaded by the time the code needs it.
So for example:
let uid = Auth.auth().currentUser?.uid
let ref = Database.database().reference()
ref.child("users").child(uid!).child("weight").observe(.value) { (snapshot) in
self.userWeight = snapshot.value as! Float
print(self.userWeight)
self.maxAmountOfWater = (self.userWeight * 4) / 100
maxWaterLabel.text = String(maxAmountOfWater)
}
Not the use of self.userWeight
is inside the callback, so it has access to the value from the database as soon as it's loaded.
If you want you can also define your own completion handler, that you then call from within the Firebase completion handler, or use a dispatch group. For some examples of this, see:
Edit:
Just a note that UI updates within a Firebase Closure are called on the main thread. So for example, if you load a tableView dataSource within the closure, you can call tableView.reloadData() within the closure as well without having to use a dispatch group or other thread.