0

I'm trying to use DispatchQueue to get my code to wait until a query retrieves the results I need from Cloud Firestore before it continues executing, but just haven't been able to get it to work. In the code below I am trying to get it to wait until the data has been retrieved and stored in the zoneMarkerArray, and then print out the result.

I've numbered each line it prints in the order that I want it to happen, and as you'll see in the output it is not waiting for the Firestore result before moving on.

Here is my code:

let zones = self.db.collection("zones")

let zonesQuery = zones.whereField("start", isGreaterThan: lowerLimit).whereField("start", isLessThan: upperLimit)

print("1. zones Query has been defined")

//pass zonesQuery query to getZoneMarkers function to retrieve the zone markers from Firestore      
getZoneMarkers(zonesQuery)

print("6. Now returned from getZoneMarkers")


func getZoneMarkers(_ zonesQuery: Query) -> ([Double]) {
    print("2. Entered getZoneMarkers function")
    DispatchQueue.global(qos: .userInteractive).async {         

        zonesQuery.getDocuments() { (snapshot, error) in

            if let error = error {
            print("Error getting zone markers: \(error)")
            } else {

                print("3. Successfully Retrieved the zone markers")
            var result: Double = 0.0

                for document in snapshot!.documents {

                    print("Retrieved zone marker is \(document["start"]!)")
                    self.zoneMarkerArray.append(document["start"]! as! Double)
                    print("4. Looping over zone marker results")

                }
            }
        }

        DispatchQueue.main.async { 
      //I want this the printCompleted function to print the result AFTER the results have been retrieved  
            self.printCompleted()

        }
    }   

    return self.zoneMarkerArray

}


func printCompleted() {
    print("5. Looping now completed. Result was \(zoneMarkerArray)")
}

And here is the output that prints out:

  1. zones Query has been defined
  2. Entered getZoneMarkers function
  3. Now returned from getZoneMarkers
  4. Looping now completed. Result was [0.0]
  5. Successfully Retrieved the zone markers
  6. Looping over zone marker results
  7. Looping over zone marker results Retrieved zone marker is 12.0
  8. Looping over zone marker results

Thanks for the help!

EDIT: In case anyone else out there is also struggling with this, here's the working code I put together in the end based on the feedback I received. Please feel free to critique if you see how it could be further improved:

let zones = self.db.collection("zones")

let zonesQuery = zones.whereField("start", isGreaterThan: lowerLimit).whereField("start", isLessThan: upperLimit)


print("1. zones Query has been defined")

//pass zonesQuery query to getZoneMarkers function to retrieve the zone markers from Firestore      
getZoneMarkers(zonesQuery)

func getZoneMarkers(_ zonesQuery: (Query)) {
    print("2. Entered getZoneMarkers function")
  zoneMarkerArray.removeAll()

    zonesQuery.getDocuments(completion: { (snapshot, error) in
        if let error = error {
            print("Error getting zone markers: \(error)")
            return
        }

        guard let docs = snapshot?.documents else { return }

        print("3. Successfully Retrieved the zone markers")


        for document in docs {

            self.zoneMarkerArray.append(document["start"]! as! Double)
            print("4. Looping over zone marker results")

        }

        self.completion(zoneMarkerArray: self.zoneMarkerArray)

    })
}


func completion(zoneMarkerArray: [Double]) {
    print("5. Looping now completed. Result was \(zoneMarkerArray)")   

}
vadian
  • 274,689
  • 30
  • 353
  • 361
MarcE
  • 69
  • 8
  • `DispatchQueue.main.async` should probably be inside the `zonesQuery.getDocuments()` closure – MadProgrammer Feb 10 '20 at 22:34
  • 2
    You can't `return` from an asynchronous method. Even if you did correctly implement the wait there is a good chance that you would end up blocking the main queue. You should pass a completion closure to your `getZoneMarkers` function and pass then call that closure with your retrieved markers. See https://stackoverflow.com/questions/25203556/returning-data-from-async-call-in-swift-function – Paulw11 Feb 10 '20 at 22:58
  • you really need to take time and understand how asynch methods work: https://firebase.googleblog.com/2018/07/swift-closures-and-firebase-handling.html – timbre timbre Feb 10 '20 at 23:56
  • Why have you used QOS : userInteractive?. – Paresh. P Feb 11 '20 at 09:54
  • Thanks for all the suggestions! I looked into everything you guys said and also the proposed answers below and decided the best approach for me was to ditch the DispatchQueue and use a completion handler. Had to read a bunch of different blogs on them as I found it thoroughly confusing but finally got it working. @Paresh.P - the QOS: userinteractive was just me playing around to see if changing the priority had any impact on the execution. – MarcE Feb 12 '20 at 16:03

2 Answers2

0

Maybe this can help you. I have a lot of users, it appends to my Model, and can check when I have all the data und go on with my code:

func allUser (completion: @escaping ([UserModel]) -> Void) {

let dispatchGroup = DispatchGroup()
var model = [UserModel]()

let db = Firestore.firestore()
let docRef = db.collection("users")
dispatchGroup.enter()
docRef.getDocuments { (querySnapshot, err) in

    for document in querySnapshot!.documents {
        print("disp enter")
        let dic = document.data()
        model.append(UserModel(dictionary: dic))
    }
     dispatchGroup.leave()
}
dispatchGroup.notify(queue: .main) {
    completion(model)
    print("completion")
}

}

JohnDole
  • 525
  • 5
  • 16
0

From the question, it doesn't appear like any DispatchQueue's are needed. Firestore asynchronous so data is only valid inside the closures following the firebase function. Also, UI calls are handled on the main thread whereas networking is on the background thread.

If you want to wait for all of the data to be loaded from Firestore, that would be done within the closure following the Firestore call. For example, suppose we have a zones collection with documents that store start and stop indicators

zones
   zone_0
      start: 1
      stop:  3
   zone_1
      start: 7
      stop:  9

For this example, we'll be storing the start and stop for each zone in a class array of tuples

var tupleArray = [(Int, Int)]()

and the code to read in the zones, populate the tupleArray and then do the 'next step' - printing them in this case.

func readZoneMarkers() {
    let zonesQuery = self.db.collection("zones")
    zonesQuery.getDocuments(completion: { documentSnapshot, error in
        if let err = error {
            print(err.localizedDescription)
            return
        }

        guard let docs = documentSnapshot?.documents else { return }

        for doc in docs {
            let start = doc.get("start") as? Int ?? 0
            let end = doc.get("end") as? Int ?? 0
            let t = (start, end)
            self.tupleArray.append(t)
        }

        //reload your tableView or collectionView here,
        //    or proceed to whatever the next step is
        self.tupleArray.forEach { print( $0.0, $0.1) }
    })
}

and the output

1 3
7 9

Because of the asynchronous nature of Firebase, you can't 'return' from a closure, but you can leverage a completion handler if needed to pass the data 'back' from the closure.

Jay
  • 34,438
  • 18
  • 52
  • 81
  • Thanks @Jay this was helpful. Based on everyone's input I got it working using a completion handler - I'll edit my original question to include what I ended up doing. – MarcE Feb 12 '20 at 16:24