1

I am making an iOS app in Swift with user data stored in a Firebase Firestore database. The user documents in Firestore are named by the UID's of the users. To get the data from Firestore, I have the function getUserData:

static func getUserData(uid: String) -> [String : Any] {
        
        let userRef = Firestore.firestore().collection(Constants.Firestore.Collections.users)
        let userDocRef = userRef.document(uid)
        
        var temp: [String : Any] = [:]
        
        userDocRef.getDocument { (document, error) in
            guard let document = document, document.exists else {
                print("document does not exist")
                return
            }
                
            let dataDesc = document.data()
            
                
            temp = dataDesc!

        }
    
        return temp
        
    }

I call this method in an initializer for my user class:

class User {
    // MARK: - Properties
    var firstName: String
    var lastName: String
    let uid: String
    let phone: String
    var available: Bool
    var friends: [User] = []

    init?(firestoreUID: String) {
                
        self.uid = firestoreUID
        
        let data = UserService.getUserData(uid: firestoreUID)
        print(data.keys)
        
        guard let phone = data[Constants.Firestore.Keys.phone] as? String,
            let available = data[Constants.Firestore.Keys.available] as? Bool,
            let firstName = data[Constants.Firestore.Keys.firstName] as? String,
            let lastName = data[Constants.Firestore.Keys.lastName] as? String
            else {
                return nil
        }
        
        self.phone = phone
        self.available = available
        self.firstName = firstName
        self.lastName = lastName
    }
}

However, when I unwrap the user instance, it gives a nil value. When I printed the keys of the retrieved dictionary from the getUserData function, the console shows only an empty array. What am I doing wrong, and how can I fix this so that I actually get my users' data from Firestore?

Edit: I've changed getUserData to boss's answer, but I'm not sure what should be in my User class init. I tried the call format suggested with ``` UserService.getUserData(uid: firestoreUID) { (data) in

        if let data = data {
            print(data.keys)
            temp = data
            
        } else {
            print("User not found")
            return
        }
        
    }

But I still get a "Fatal error: Unexpectedly found nil when unwrapping an optional value" when I try to query the temp dictionary.

byfu34
  • 61
  • 9
  • Don't put this asynchronous method inside an object's initializer. Get the data first and then initialize the object with it. – trndjc Jun 16 '20 at 15:04
  • Made the change, but still got nil being unwrapped, so I checked and realized that my code within the completion block in the getUserData call is never being executed. From what I found online, this problem is usually called by not calling completion() in the function definition, which I definitely am doing. Any advice? – byfu34 Jun 16 '20 at 15:34
  • Does this answer your question? [Returning data from async call in Swift function](https://stackoverflow.com/questions/25203556/returning-data-from-async-call-in-swift-function) – Joakim Danielson Jun 22 '20 at 17:36
  • The answer from @bsod is very good but it may not be the best solution for this use case, and it's using a completion handler and that doesn't seem to be what you want here. I appears you're attempting to read data from Firebase and populate a User object - the question is, what happens next? e.g. Is this part of a application authentication function where the user logs in, you read their data from Firebase and then maybe present them a task list or some other view? What's the next step that happens once the user object is populated? – Jay Jun 22 '20 at 20:00
  • The very next step is to access the properties of the user object to display a custom UILabel. I think I got bsod's answer to work for the label, but part of my application includes a friends list, and getting a full list with the async getData is proving somewhat difficult for me, albeit I am very new at this. – byfu34 Jun 23 '20 at 12:39
  • As mentioned that answer won't work well for this use case and you have a number of other issues in the code. For example this `return temp` will be called before the firebase data has returned from the server (most of the time). This `let data = UserService.getUserData(uid: firestoreUID)` is not how to work with a function that has a closure/completion handler. – Jay Jun 24 '20 at 17:03

1 Answers1

3

EDIT: Swift now allows functions to return asynchronously. If your app doesn't support Swift's async/await, then you must use the completion-handler model described below.


userDocRef.getDocument is asynchronous, meaning that the call to get the document is immediately followed by the next line in your code return temp. It doesn't wait for the database to fetch the document so you will always return nil; that's how async works (versus sync).

In Swift, you cannot (or definitely should not) create a function that returns something asynchronously. Instead, the function should have an escaping completion handler that returns the async object as one of its arguments.

static func getUserData(for uid: String, completion: @escaping (_ data: [String: Any]?) -> Void) {
    let userRef = Firestore.firestore().collection(Constants.Firestore.Collections.users)
    let userDocRef = userRef.document(uid)

    userDocRef.getDocument { (snapshot, error) in
        if let doc = snapshot,
           doc.exists {
            completion(doc.data())
        } else {
            if let error = error {
                print(error)
            }
            completion(nil)
        }
    }
}

Usage

getUserData(for: userId) { (data) in
    if let data = data {
        ...
    } else {
        ...
    }
}
trndjc
  • 11,654
  • 3
  • 38
  • 51