-1

Every time I test my code, an empty string is returned to the pickerUI rather than the college name. Why is that? When debugging, docData is set correctly but changes back to an empty string after the closure.

var ans = "";
var pickerData = [Any?]();
let db = Firestore.firestore();

override func viewDidLoad() {
    super.viewDidLoad();
    let docRef = db.collection("colleges").document("UMD");
    var docData = "";
    docRef.getDocument {  ( document, error) in
        if error == nil {
            docData = document!.get("Name") as! String;

        } else{

        }
    }
    pickerData.append(docData);
    picker.delegate = self
    picker.dataSource = self
}
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • Because `getDocument` works asynchronously. Code doesn't run necessarily in order of the lines. And – unrelated – `Any?` is the worst type in Swift. – vadian May 18 '20 at 19:39
  • http://www.programmingios.net/returning-a-value-from-asynchronous-code/ – matt May 18 '20 at 19:53

1 Answers1

0

It's because data is loaded from Firestore asynchronously, and your main code continues to run while that is happening. It's easiest to see by placing a few log statements:

let docRef = db.collection("colleges").document("UMD")
print("Before starting to get data")
docRef.getDocument {  ( document, error) in
    print("Got data")
}
print("After starting to get data")

When you run this code, it prints:

Before starting to get data

After starting to get data

Got data

This is probably not the order you expected, but it does completely explain why your code doesn't work. By the time your pickerData.append(docData) runs, the docData = document!.get("Name") as! String hasn't run yet.

For this reason, any code that needs data from the database needs to be inside the closure, or be called from there:

let docRef = db.collection("colleges").document("UMD")
var docData = ""
docRef.getDocument {  ( document, error) in
    if error == nil {
        docData = document!.get("Name") as? String ?? "No Name"

        pickerData.append(docData)
        picker.delegate = self
        picker.dataSource = self
    } else{

    }
}

Also see:

Community
  • 1
  • 1
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • Downvoter: can you please explain why you downvoted? – Frank van Puffelen May 18 '20 at 20:53
  • Great answer! Not sure why it was downvoted. You don't need the ; after each line. Also safely unwrapping the optionals is probably a good idea `guard let doc = document else { return }` and then `let docData = doc.get("Name") as? String ?? "No Name"` – Jay May 19 '20 at 20:03
  • Hey Jay. Good point on the semicolons, I removed them. I'm leaving the hard-cast, as the closure is guaranteed to *either* get an error *or* a document. The hard-cast can only fail if that guarantee isn't met, which... uhm... isn't what guarantees are about. :) – Frank van Puffelen May 19 '20 at 20:59
  • Agreed! I wonder why the [docs](https://firebase.google.com/docs/reference/swift/firebasefirestore/api/reference/Type-Definitions.html#firdocumentsnapshotblock) show both parameters of the FIRDocumentSnapshotBlock as optional - I guess if there *is* an error then the snapshot could be nil? Anyhoo.. this part though `.get("Name")` needs it as if a document doesn't have a Name field, it will crash. – Jay May 19 '20 at 21:16
  • Oh, I don't like this API signature very much. Flutter has a single wrapper object for this type of async data, so that is *never* null. But I prefer two callbacks even better, like RTDBs `onDataChanged` and `onCancelled` (in Android, but similar logic for all platforms). Agreed on guarding the other cast, so I updated it in my answer. I'm clearly a polyglot, which means that I'm equally non-idiomatic in every language. :-D – Frank van Puffelen May 19 '20 at 21:58