How to Handle Async Requests in Swift

How to handle asynchronous http request in Swift

A few observations:

  1. You should pull this network code into its own method.
  2. You are specifying the request in both the body and the URL. The body is sufficient. Remove it from the URL.
  3. You should excise the NSMutableData (use Data), NSMutableURLRequest (use URLRequest), NSURL (use URL), NSArray, NSDictionary, etc. It’s generally best to stay within the Swift types where possible.
  4. Rather than manually iterating through JSONSerialization results, use JSONDecoder.
  5. You also need to percent encode the body of your request, as bus_stop=Science Hill is not valid in a x-www-form-urlrequest. See https://stackoverflow.com/a/26365148/1271826.
  6. You said:

    My goal would be to fill the bus array with the http response data, then print it

    You need to move your code dependent upon the request inside the completion handler closure.

Thus:

func fetchBuses(completion: @escaping (Result<[Bus], Error>) -> Void) {
let headers = [
"content-type": "application/x-www-form-urlencoded",
"cache-control": "no-cache",
"postman-token": "23cb4108-e24b-adab-b979-e37fd8f78622"
]

let url = URL(string: "https://ucsc-bts3.soe.ucsc.edu/bus_stops/inner_eta.php")!
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 10)

request.httpBody = ["bus_stop": "Science Hill"].percentEncoded()
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers

let session = URLSession.shared
let dataTask = session.dataTask(with: request) { data, response, error in
guard
error == nil,
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
completion(.failure(error ?? BusError.unknown(data, response)))
return
}

do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let responseObject = try decoder.decode(ResponseObject.self, from: responseData)
completion(.success(responseObject.rows))
} catch let jsonError {
completion(.failure(jsonError))
}
}
dataTask.resume()
}

And

func mapView(_ mapView: MGLMapView, annotation: MGLAnnotation, calloutAccessoryControlTapped control: UIControl) {
fetchBuses { result in
switch result {
case .failure(let error):
print(error)

case .success(let buses):
print(buses)

DispatchQueue.main.async {
mapView.deselectAnnotation(annotation, animated: false)
let schedule = ScheduleVC()
schedule.data.append(annotation.title!! + " ETAs")
self.present(schedule, animated: true, completion: nil)
}
}
}
}

Where

enum BusError: Error {
case unknown(Data?, URLResponse?)
}

struct Bus: Decodable {
let busId: Int
let busType: String
let nextBusStop: String
let timeAway: Int
}

struct ResponseObject: Decodable {
let rows: [Bus]
}

And

extension Dictionary {
func percentEncoded() -> Data? {
return map { key, value in
let escapedKey = "\(key)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
let escapedValue = "\(value)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
return escapedKey + "=" + escapedValue
}
.joined(separator: "&")
.data(using: .utf8)
}
}

extension CharacterSet {
static let urlQueryValueAllowed: CharacterSet = {
let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
let subDelimitersToEncode = "!$&'()*+,;="

var allowed = CharacterSet.urlQueryAllowed
allowed.remove(charactersIn: generalDelimitersToEncode + subDelimitersToEncode)
return allowed
}()
}

async requests on Swift using completion handler and DispatchSemaphore

Is your intention here to create a synchronous function that will block and wait until the async request completes?

If so, you're almost there but you need to change the Semaphore to start with 0 rather than 1 (decrementing from 1 to 0 won't stop execution, it needs to go negative), and you need to move the wait() call outside the closure, otherwise you aren't stopping the function from returning and instead would be blocking your closure from ever completing.

func customRequest(zipCode: String) -> Bool {
var response = false

let dispatchQueueProcess = DispatchQueue.global(qos: .userInitiated)
let semaphore = DispatchSemaphore(value: 0)

// Start async work on background thread, current function's thread
// execution point will then immediately move to the line
// after the closing brace
dispatchQueueProcess.async {
apiRequest(zipCode: zipCode) { apiResponse in
if let apiResponse = apiResponse {
response = apiResponse
} else {
print("Server did not Response")
}

// Tell the original function's thread that it is OK to continue
semaphore.signal()

}
}

// Immediately wait for the semaphore after starting the async work so that
// the customRequest(zipCode:) function pauses here until the semaphore is
// signalled in the closure.
semaphore.wait()

// Once the semaphore.signal() call happens in the async closure execution
// resumes, and the response variable can now be returned with the updated
// value.
return response
}

You might find that you don't actually want to make a synchronous function like this, because if you call this from the main queue you will freeze the UI until the response is received.

Edit: This example should copy-paste run in a Playground

import Foundation

// template function to simulate the API call
func apiRequest(zipCode: String, response: (Bool?)->Void) {
sleep(3)
response(true)
}

func customRequest(zipCode: String) -> Bool{
var response = false

let dispatchQueueProcess = DispatchQueue.global(qos: .userInitiated)
let semaphore = DispatchSemaphore(value: 0)

dispatchQueueProcess.async {
apiRequest(zipCode: zipCode) { apiResponse in
if let apiResponse = apiResponse {
response = apiResponse
} else {
print("Server did not Response")
}
semaphore.signal()

}
}

semaphore.wait()

return response
}

print("Result: \(customRequest(zipCode: "90030"))")

How to make async / await in Swift?

Thanks to vadian's comment, I found what I expected, and it's pretty easy. I use DispatchGroup(), group.enter(), group.leave() and group.notify(queue: .main){}.

func myFunction() {
let array = [Object]()
let group = DispatchGroup() // initialize

array.forEach { obj in

// Here is an example of an asynchronous request which use a callback
group.enter() // wait
LogoRequest.init().downloadImage(url: obj.url) { (data) in
if (data) {
group.leave() // continue the loop
}
}
}

group.notify(queue: .main) {
// do something here when loop finished
}
}

How to call function after async requests finish in SwiftUI?

I tried using this snippet of code within my second api request and it solved my issue, although i do not understand why I need to do this

DispatchQueue.main.async {
self.venueData.venuesdataarray.append(RESPONSE_DETAILS_HERE)
}

Originally I had asked, Does anyone know how I can update my @EnvironmentObject after all the requests complete?

Does anyone know why the snippet I have above makes everything work? Id just like to understand what im doing and maybe someone could learn something if they find this

Wait until swift for loop with asynchronous network requests finishes executing

You can use dispatch groups to fire an asynchronous callback when all your requests finish.

Here's an example using dispatch groups to execute a callback asynchronously when multiple networking requests have all finished.

override func viewDidLoad() {
super.viewDidLoad()

let myGroup = DispatchGroup()

for i in 0 ..< 5 {
myGroup.enter()

Alamofire.request("https://httpbin.org/get", parameters: ["foo": "bar"]).responseJSON { response in
print("Finished request \(i)")
myGroup.leave()
}
}

myGroup.notify(queue: .main) {
print("Finished all requests.")
}
}

Output

Finished request 1
Finished request 0
Finished request 2
Finished request 3
Finished request 4
Finished all requests.

How to make inner async request complete first before completing outer async request in Swift?

One way to do this is to change methodTwo() to accept a callback as an argument, then you can use a semaphore:

func methodOne(urlString1: String) {
let targetURL = NSURL(string: urlString1)
let task = NSURLSession.sharedSession().dataTaskWithURL(targetURL!) { data, response, error in
let queue = dispatch_queue_create("org.myorg.myqueue", nil)
dispatch_async(queue) {

// DO STUFF
j = some value

print("Inside Async1")
for k in j...someArray.count - 1 {
print("k = \(k)")

print("Calling Async2")
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
self.methodTwo("some url string") {
dispatch_semaphore_signal(sem);
}
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
}
}
}
task.resume()
}

func methodTwo(urlString2: String, callback: (() -> ())) {
let targetURL = NSURL(string: urlString2)
let task = NSURLSession.sharedSession().dataTaskWithURL(targetURL!) { data, response, error in

// DO STUFF
print("inside Async2")
callback()
}
task.resume()
}

Note that to not block the delegate queue of methodOne's task callback, the example creates its own queue that you can block at will.



Related Topics



Leave a reply



Submit