0

I have the following functions. I'm trying to pass allItems array into the completion block of requestItems, but I get a crash as it says it's nil. I removed the completion to check that item has a value and it does.

That means that the completion executes before the for loop.

Is there another approach for this? Something like Promises in Javascript that will execute the completion when the for loop has finished.

func requestItems(_ data: [String: Any], completion: (Bool, [Item]) -> Void) {

    var allItems = [Item]()

    for i in data["all"] {

        Routes.instance.getRequest(requestType: "items", params: nil, id: someId, completion: { item in

            var it = Item(item["name"] as! String)
            allItems.append(it)

        })

    }

    completion(true, allItems)
}

func getRoutes(requestType: String, parameters: [String: Any]?, id: String, completion: @escaping ([[String:Any]]) -> Void) {

    DispatchQueue.main.async {

        if id == "" {
            self.url = "\(URL_BASE)/\(requestType)"

        } else {
            self.url = "\(URL_BASE)/\(requestType)/\(id)"
        }

        Alamofire.request(self.url, method: .get, parameters: parameters, encoding: JSONEncoding.default, headers: self.headers).responseJSON { response in

            guard response.result.error == nil else {
                print(response.result.error!)
                return
            }

            switch response.result {

            case .success(let JSON):
                let response = [JSON] as! NSArray

                for item in response {

                    if let data = item as? [String: Any] {
                        print(data)
                    }
                }

                completion(response as! [[String : Any]])

            case .failure(let error):
                print("Request failed with error: \(error)")
            }
        }
    }

}

The completion handler executes too soon, returning a nil item

  • Why is `completion(true, allItems)` called BEFORE you did all the `allItems.append(it)`? There is no direct call (visible) between the two methods? Use https://stackoverflow.com/questions/34983370/swift-dispatch-group-wait-not-waiting – Larme Aug 03 '17 at 12:50
  • 1
    Related: https://stackoverflow.com/questions/41809000/call-completion-block-when-two-other-completion-blocks-have-been-called – Ahmad F Aug 03 '17 at 13:25

2 Answers2

5

You need DispatchGroup to get notified when the asynchronous loop is finished for example:

func requestItems(_ data: [String: Any], completion: (Bool, [Item]) -> Void) {

    var allItems = [Item]()
    let group = DispatchGroup()
    for i in data["all"] {
        group.enter()
        Routes.instance.getRequest(requestType: "items", params: nil, id: someId, completion: { item in

            let it = Item(item["name"] as! String)
            allItems.append(it)
            group.leave()

        })
    }
    group.notify(queue: DispatchQueue.main) {
        completion(true, allItems)
    }
}
vadian
  • 274,689
  • 30
  • 353
  • 361
  • @SalmanGhumsani Once (1x) after the last iteration of the loop has called `group.leave()` – vadian Aug 03 '17 at 13:39
  • means it will call only once right? when the last iteration? also can you explain the functionality of this how it will know that is a last iteraion.. – Salman Ghumsani Aug 03 '17 at 13:41
  • Yes and Yes (**after** the last iteration) Simple circumscription: The group counts the occurrences of enter (+1) and leave (-1) and exits if the counter reaches zero. – vadian Aug 03 '17 at 13:44
  • Initial count 0 and occurrences of enter (+1) `means count == 1` and leave (-1) means `count == 0` so it will call each time.. – Salman Ghumsani Aug 03 '17 at 13:48
  • 1
    No, the loop is much faster than the asynchronous response, so it increments a couple of times before the first decrement. Maybe the actual mechanism under the hood is different but this is the principle. Please write some code in a Playground, add print lines and prove yourself. – vadian Aug 03 '17 at 13:52
  • Okay got it, The enter call on main thread but leave not. Thanks a lot for this :) – Salman Ghumsani Aug 03 '17 at 13:52
  • Thanks, I've been playing around with this the whole afternoon. I had used this in the past and wanted to try a different approach. There seems to be a lot of difference if `requestItems` is inside another request in the main thread, hence I suggested Promises to guarantee that the function has finished. By testing the suggested answer, `group.notify` gets called each time a request function finishes and not when all of them finish. – Christian Ray Leovido Aug 03 '17 at 17:30
  • My answer is quite simple just to show the functionality. You can create a separate dispatch queue for the group. But basically the notify closure is supposed to be called once. – vadian Aug 03 '17 at 17:38
  • Excellent answer, as always. But, I’d suggest being a little careful here if the order of the responses is relevant. In this code snippet, the order of `allItems` may bear no relation to the order of the `data["all"]`. Often we’d save the results in a dictionary, and then, when we’re done, rebuild the array... – Rob Jan 13 '21 at 14:59
0

You are calling your completion handler outside the completion handler of the asynchronous request getRequest, so it will obviously return before the function would finish execution. Since you are calling several asynchronous requests in a row, a simple completion handler won't do the trick.

The best approach is to either use a DispatchQueue to only let your function return when all the requests are finished or to use a 3rd party framework, such as PromiseKit to handle the async functions are normal functions with return values.

Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116