Reputation: 650
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
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
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