Ray Saudlach
Ray Saudlach

Reputation: 650

Swift: Synchronous Web Service Calls

In Swift, I am calling a Web Serice (Google Places) and succesfully getting the Google Place ID.

As I'm iterating through the JSON responses and getting Google Place ID's, I would like to call another Web Service (Google Place Details)

With the code below, I get this response I get is:

estPlace_ID_1
Return Number Is: Nothing
estPlace_ID
Return Number Is: Nothing
.....
Function Phone Number is: 867-5309
Function Phone Number is: 867-5309

It seems as if the function get Details is not being executed until the for result in results loop has finished.

How can the code be changed so it waits until getDetails is executed before continuing to iterate?

class func getDetails(id: String) -> String {
    <Setup the request>
    let session = NSURLSession.sharedSession()

    //Second Request
    let task = session.dataTaskWithRequest(request) { data, response, error in 
        do {
            //Parse Result
            print("Function Phone Number is" + phoneNumber)
        }
        catch {
        }
    }
    task.resume()

    return phoneNumber
}

//First request
<Setup the request>
let task = session.dataTaskWithRequest(request) { data, response, error in
    //a few checks with guard statements

    do {        
        //Extract results from JSON response
        results = <FROM_JSON>

        for result in results {
            estPlace_ID = result["value"]

            print(estPlace_ID)
            print("return number is" + getDetails(estPlace_ID))              
        }
        catch {       
        }
    }
    task.resume()
}

Upvotes: 2

Views: 1714

Answers (2)

Rob
Rob

Reputation: 437552

I'd suggest you adopt asynchronous patterns. For example, have a method that retrieves phone numbers asynchronous, reporting success or failure with a completion handler:

let session = NSURLSession.sharedSession()

func requestPhoneNumber(id: String, completionHandler: (String?) -> Void) {
    let request = ...

    let task = session.dataTaskWithRequest(request) { data, response, error in
        do {
            let phoneNumber = ...
            completionHandler(phoneNumber)
        }
        catch {
            completionHandler(nil)
        }
    }
    task.resume()
}

Then your first request, that retrieves all of the places, will use this asynchronous requestDetails:

// I don't know what your place structure would look like, but let's imagine an `id`,
// some `info`, and a `phoneNumber` (that we'll retrieve asynchronously).

struct Place {
    var id: String
    var placeInfo: String
    var phoneNumber: String?

    init(id: String, placeInfo: String) {
        self.id = id
        self.placeInfo = placeInfo
    }
}

func retrievePlaces(completionHandler: ([Place]?) -> Void) {
    let request = ...

    let task = session.dataTaskWithRequest(request) { data, response, error in
        // your guard statements

        do {
            // Extract results from JSON response (without `phoneNumber`, though

            var places: [Place] = ...

            let group = dispatch_group_create()

            // now let's iterate through, asynchronously updating phone numbers

            for (index, place) in places.enumerate() {
                dispatch_group_enter(group)

                self.requestPhoneNumber(place.id) { phone in
                    if let phone = phone {
                        dispatch_async(dispatch_get_main_queue()) {
                            places[index].phoneNumber = phone
                        }
                    }
                    dispatch_group_leave(group)
                }
            }

            dispatch_group_notify(group, dispatch_get_main_queue()) {
                completionHandler(places)
            }
        }
    }
    task.resume()
}

This also adopts asynchronous pattern, this time using dispatch group to identify when the requests finish. And you'd use the completion handler pattern when you call this:

retrievePlaces { phoneNumberDictionary in 
    guard phoneNumberDictionary != nil else { ... }

    // update your model/UI here
}

// but not here

Note, the retrievePlaces will issues those requests concurrently with respect to each other (for performance reasons). If you want to constrain that, you can use a semaphore to do that (just make sure to do this on a background queue, not the session's queue). The basic pattern is:

dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0)) {
    let semaphore = dispatch_semaphore_create(4) // set this to however many you want to run concurrently

    for request in requests {
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)

        performAsynchronousRequest(...) {
            dispatch_semaphore_signal(semaphore)
        }
    }
}

So that might look like:

func retrievePlaces(completionHandler: ([Place]?) -> Void) {
    let request = ...

    let task = session.dataTaskWithRequest(request) { data, response, error in
        // your guard statements

        do {
            dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0)) {
                // Extract results from JSON response
                var places: [Place] = ...

                let semaphore = dispatch_semaphore_create(4) // use whatever limit you want here; this does max four requests at a time

                let group = dispatch_group_create()

                for (index, place) in places.enumerate() {
                    dispatch_group_enter(group)
                    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)

                    self.requestPhoneNumber(place.id) { phone in
                        if let phone = phone {
                            dispatch_async(dispatch_get_main_queue()) {
                                places[index].phoneNumber = phone
                            }
                        }
                        dispatch_semaphore_signal(semaphore)
                        dispatch_group_leave(group)
                    }
                }

                dispatch_group_notify(group, dispatch_get_main_queue()) {
                    completionHandler(places)
                }
            }
        }
    }
    task.resume()
}

Frankly, when it's this complicated, I'll often use asynchronous NSOperation subclass and use maxConcurrentOperationCount of the queue to constrain concurrency, but that seemed like it was beyond the scope of this question. But you can also use semaphores, like above, to constrain concurrency. But the bottom line is that rather than trying to figure out how to make the requests behave synchronously, you'll achieve the best UX and performance if you follow asynchronous patterns.

Upvotes: 0

Cristik
Cristik

Reputation: 32793

Making a function call block until the result of an async call arrives can be achieve via a dispatch semaphore. The pattern is:

create_semaphore()
someAyncCall() {
    signal_semaphore()
}
wait_for_semaphore()
rest_of_the_code()

In your case, you can modify your getDetails method as following:

class func getDetails(id: String) -> String {
    <Setup the request>
    let session = NSURLSession.sharedSession()
    let sem = dispatch_semaphore_create(0)

    //Second Request
    let task = session.dataTaskWithRequest(request) { data, response, error in 
        do {
            //Parse Result
            print("Function Phone Number is" + phoneNumber)

        } catch {
        }
        // the task has completed, signal this
        dispatch_semaphore_signal(sem)
    }
    task.resume()

    // wait until the semaphore is signaled
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER)

    // we won't get here until dispatch_semaphore_signal() is called
    return phoneNumber
}

One important thing to keep in mind (thanks Rob for pointing this out) is that you need to call getDetails on a different queue, otherwise you'll get a deadlock:

dispatch_async(dispatch_get_global_queue(0, 0) ){
    for result in results {
        let estPlace_ID = result["value"]

        print(estPlace_ID)
        print("return number is" + getDetails(estPlace_ID))
    }
}

Note that in the above example the second parameter to dispatch_semaphore_wait is DISPATCH_TIME_FOREVER which means the calling code will be indefinitely wait for the async call to finish. If you want to set some timeout you can create a dispatch_time_t value and pass it:

// want to wait at most 30 seconds
let timeout = 30
let dispatchTimeout = dispatch_time(DISPATCH_TIME_NOW, timeout * Int64(NSEC_PER_SEC))
dispatch_semaphore_wait(sem, dispatchTimeout)

Upvotes: 1

Related Questions