1

I'm a swift and Firebase beginner, I met a problem that confuses me a lot. We all know the following code

var a = [Int]()
for i in 0..2{
    a.append(i)
    print(a)
print(a)

should give me the output

[0]
[0,1]
[0,1,2]
[0,1,2]

And it is true as the result. However, when I work on getDocument(), and I write a similar logic code, the output is strange! The code is the following.

override func viewDidLoad() {
    super.viewDidLoad()
    let docSecond = getThirtyData()
    getDataContent(thirtyData: docSecond)
}
func getThirtyData()->Array<Int>{
    var querySecond = [Int]()
    var docSecond = [Int]()
    let postDoc = db.collection("announcement")
    postDoc.getDocuments { (querySnapshot, err) in
        if let err = err{
            print("Error getting documents: \(err)")
        }else{
            for document in querySnapshot!.documents{
                //print(document.data()["postTimeSecondInt"] as! Int)
                querySecond.append(document.data()["postTimeSecondInt"] as! Int)
            }
            for _ in 1...querySecond.count{
                let max = querySecond.max()!
                docSecond.append(max)
                let index = querySecond.firstIndex(of: max)
                querySecond.remove(at: index!)
            }
            print(docSecond)
            print("123")
        }
    }
    print(docSecond)
    print("456")
    return docSecond
}

func getDataContent(thirtyData: [Int]){
    print(thirtyData)
    print("789")
}

I thought the result might come as same logic which is "123" print first, then "456", then "789". However, the result is like below.

[]
456
[]
789
[1588428987, 1588428980, 1588427392]
123

Seems like it run the code line below simultaneously with the for loop. Can anyone explain why this happens?

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
Yi-An
  • 93
  • 10

1 Answers1

1

This is working as intended/documented, but if you've never used an asynchronous API it can indeed be confusing. The reason that the code executes out of order is that postDoc.getDocuments() has to read data from the server, which may take time. Instead of blocking your code (and thus keeping the user from using your app), your main code is allowed to continue. And then when the data is available from the server, your completion handler is called with that data.

It's easiest to see this by adding some logging statements:

print("Before starting to load data")
let postDoc = db.collection("announcement")
postDoc.getDocuments { (querySnapshot, err) in
    print("Got data");
}
print("After starting to load data")

When you run this minimal code, it prints:

Before starting to load data

After starting to load data

Got data

This is working as intended, but indeed very confusing if you've never worked with asynchronous or event driven APIs yet. Unfortunately most modern cloud/web APIs are asynchronous, so it's best to get used to this pattern. Hopefully my explanation above help a bit for that.

Two important things to remember:

  • Your print(docSecond) doesn't print the data you want, because docSecond.append(max) hasn't run yet.
  • Any code that needs data from the database, needs to be inside the completion handler that is called with that data (or be called from there).

You can't return data that is loaded asynchronously. The typical workaround is to create a custom completion handler, very similar to the completion handler that Firestore's getDocuments() accepts postDoc.getDocuments { (querySnapshot, err) in, but then with your own type.

Something like:

func getThirtyData(_ completion: (Array<Int>) -> ()) {
    var querySecond = [Int]()
    var docSecond = [Int]()
    let postDoc = db.collection("announcement")
    postDoc.getDocuments { (querySnapshot, err) in
        if let err = err{
            print("Error getting documents: \(err)")
        }else{
            for document in querySnapshot!.documents{
                querySecond.append(document.data()["postTimeSecondInt"] as! Int)
            }
            for _ in 1...querySecond.count{
                let max = querySecond.max()!
                docSecond.append(max)
                let index = querySecond.firstIndex(of: max)
                querySecond.remove(at: index!)
            }
            completion(docSecond)
        }
    }
}

And then call it like:

getThirtyData { docSecond in
   getDataContent(thirtyData: docSecond)
}

See also:

Community
  • 1
  • 1
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • Thank you so much, helps me a lot! I'm new to software engineer, the concept of asynchronous is new to me. Thanks for teaching me a lesson! – Yi-An May 03 '20 at 02:57
  • You're welcome. This is one of the major stumbling blocks for many devs new to asynchronous APIs, so don't give up if you keep falling for it. It took me a few years before such APIs became second nature. – Frank van Puffelen May 03 '20 at 03:43