Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Asynchronous completion handling in a function with multiple closures/API requests in swift

I just started developing in Swift, so im totally new to closures. I'm also new how to handle asynchronous API request.

I have read a lot of similar question such as, How to get data to return from NSURLSessionDataTask in Swift and How to use completionHandler Closure with return in Swift?. These helped me, but my problem it a little bit different.

In my function I want to first make a API request to get a JSON payload. With some data in this JSON payload I want to make multiple other API request. In this case, I will for each of API request receive a JSON payload, where I want to store some of the data in my own JSON data structure.

The problem is that, for every multiple API request I make I can only return part of my own JSON data in my CompletionHandler - This is only way to return data when making an API request using a closure, as far as I understand.

So instead of getting multiple completion handlers, when calling my function, I just want to receive a single.

The thing is I dont know to how to completion handling several closures in a function, in this case two closures.

I have posted my code below - I know its quite long and maybe not that clean. However, the point is that when im updating offers to my storeDict this will be empty, due to the offers dict array is getting its information from inside the second closure. This is shown at the bottom of the function.

   func getOffersFromWishList(offerWishList: [String], latitude: Double, longitude: Double, radius: Int, completionHandler: ([NSDictionary] -> Void)) {

        var master: [NSDictionary] = []

        var nearby_params: NSDictionary = ["r_lat": latitude, "r_lng": longitude, "r_radius": radius]
        //println(nearby_params)
        var store_id_list: [String] = []


        // Get all store_ids for store which are nearby (Radius determines how nearby)
        singleton_eta.api("/v2/stores", type: ETARequestTypeGET, parameters: nearby_params, useCache: true, completion: { (response, error, fromCache) -> Void in

            if error == nil {

                let json = JSON(response)
                storeArray = json.arrayValue
                //println(storeArray)

                for store in storeArray {

                    var storeDict = [String: AnyObject]()
                    var metaData = [String: String]()
                    var offers: [NSDictionary] = []

                    let name = store["branding"]["name"].stringValue
                    let store_id = store["id"].stringValue
                    let street = store["street"].stringValue
                    let city = store["city"].stringValue
                    let zip_code = store["zip_code"].stringValue
                    let dealer_id = store["dealer_id"].stringValue
                    let logo = store["branding"]["logo"].stringValue

                    metaData = ["name": name, "store_id": store_id, "street": street, "city": city, "zip_code": zip_code, "dealer_id": dealer_id, "logo": logo]

                    store_id_list.append(store_id)

                    //println("Butiks ID: \(store_id)")

                    var offset = 0
                    let limit = 100

                    // Loop through the offers for the specific store id - only possible to request 100 offers each time
                    // A while loop would be more suitable, but I dont know when to stop, as the length of the offerArray can not be counted as it is cant be accessed outside of the closure.
                    for x in 1...2 {

                        var store_params: NSDictionary = ["store_ids:": store_id, "limit": limit, "offset": offset]
                        println(store_params)
                        // Get offers for a specific store_id
                        singleton_eta.api("/v2/offers", type: ETARequestTypeGET, parameters: store_params, useCache: true, completion: { (response, error, fromCache) -> Void in

                            if error == nil {

                                offerArray = JSON(response).arrayValue
                                //println( "TypeName0 = \(_stdlib_getTypeName(offerArray))")

                                //Loop through the recieved offers
                                for of in offerArray {

                                    let name = of["branding"]["name"].stringValue
                                    let dealer_id = of["dealer_id"].stringValue
                                    let heading = of["heading"].stringValue
                                    let description = of["description"].stringValue
                                    let price = of["pricing"]["price"].stringValue
                                    let image = of["images"]["view"].stringValue


                                    //println(heading)

                                    // Loop through our offerWishList
                                    for owl in offerWishList {

                                        let headingContainsWish = (heading.lowercaseString as NSString).containsString(owl.lowercaseString)

                                        // Check if offer match with our wish list
                                        if(headingContainsWish) {

                                            // Save neccesary meta data about each offer to a tuple array
                                            var offer = Dictionary<String, String>()
                                            offer = ["name": name, "dealer_id": dealer_id, "heading": heading, "description": description, "price": price, "image": image, "offerWishItem": owl]

                                            offers.append(offer)


                                        }

                                    }

                                }


                            }

                        })
                        //println(storeDict)
                        offset = offset + limit + 1

                    }

                    storeDict.updateValue(metaData, forKey: "meta_data")
                    storeDict.updateValue(offers, forKey: "offers")  // offers is empty due to its appending inside the closure
                    master.append(storeDict)

                }
             completionHandler(master)

            }

            else {
                println(error)
            }


        })

    }

Calling the above function

getOffersFromWishList(offerWishList, latitude, longitude, radius) { (master) -> Void in
       println(master)
    }

This is what the master will print when calling the function, where offers is empty.

{
"meta_data" =     {
    city = "Kongens Lyngby";
    "dealer_id" = d8adog;
    logo = "https://d3ikkoqs9ddhdl.cloudfront.net/img/logo/default/d8adog_3qvn3g8xp.png";
    name = "d\U00f8gnNetto";
    "store_id" = d2283Zm;
    street = "Kollegiebakken 7";
    "zip_code" = 2800;
};
offers =     (
);
}
{ 
  ...
}

So my questions, what is the proper way to return data from the second closure to the first closure inside a function? Or am I doing this in the completely wrong way? The thing is, I need all this data for a tableview and therefore need all the data at once.

like image 727
Henrik Holm Avatar asked Apr 08 '15 17:04

Henrik Holm


1 Answers

A couple of thoughts:

  1. If there's any possibility of returning all of this in a single request to the server, that might offer better performance. Often, the time required to performing the requests on server is inconsequential in comparison to the network latency. If you can avoid needing to make one request, get a response, and then issue more requests, that would be ideal.

    Or perhaps you request the locations within some distance in advance, cache that, and then the "show me deals for nearby locations" might not require these two sets of requests.

    (I recognize that neither of these may work for you, but it's something to consider if you can. If you can eliminate consecutive requests and focus on largely concurrent requests, you'll have much better performance.)

  2. Let's assume for a second that the above is not an option, and you're stuck with one request to get the nearby locations and another set to get the deals. Then you have a couple of options:

    • You can definitely go down the road that you're contemplating with a single callback. You can, for example, issue all of your requests, doing a dispatch_group_enter before you initiate each request, do a dispatch_group_leave upon the completion of each request, and then issue a dispatch_group_notify that will be called when each enter call has been offset by a corresponding leave call. So, build your response object as each request finishes, and only when they're done, call the completion closure.

    • Another approach would be to have a closure that behaves more like an enumeration closure, one that is called as each site's deals come in. That way, the UI can be updated as things come in, rather than waiting for everything. If you're on a slow network, updating the UI as data comes in may be far more tolerable. (E.g., consider ten requests, each which takes 1 second complete on a slow 3G cellular connection: watching them pop in one per second is far more tolerable than seeing nothing for ten seconds).

    • Having said that, you may want to abandon closures completely. You could consider a delegate-protocol pattern, where you specify a delegate for your request, and then implement protocol methods for each of the responses you get from the server. That way you can update the UI as new responses come in, rather than holding everything up until the last one comes in. But we're recognizing that there are very different types of responses (one is a list of sites, another is the list deals for a given site, a third would be the "I'm all done" and/or "there was an error), so when it starts to get this complicated, it might be better to define a protocol for this interface, and handle it that way.

like image 123
Rob Avatar answered Oct 21 '22 15:10

Rob