Wait Until an Asynchronous API Call Is Completed - Swift/Ios

Waiting until an asynchronous call is complete before running the override functions of a View Controller (Swift)


Due to the asynchronous nature of the function, I do not know how to make the application "wait" for the data fetching to complete before running cellForRowAtIndexPath

Well, cellForRowAtIndexPath will automatically "not execute", when there are no data supplied to the table view. Remember that this is a table view data source method. You don't get to call it directly. So just implement cellForRowAtIndexPath as you would normally. If numberOfRowsInSection returns 0, then cellForRowAtIndexPath would not be called, would it?

I would imagine you have implemented numberOfRowsInSection with a line like this:

return dataSource.count

where dataSource is a property of your VC, of type [DataPoint]. Initially, set this dataSource property to an empty array:

var dataSource = [DataPoint]()

And when the table view loads, it will display nothing, because the data has not been fetched. Now in viewDidLoad, we call this async function to fetch the data:

retrieveDataFromAPI(tic: "some string") {
dataFetched in
DispatchQueue.main.async {
self.dataSource = dataFetched
self.tableView.reloadData() // this calls the table view data source methods again
}
}

At the time you call reloadData, dataSource will no longer be empty, and cellForRowAtIndexPath will be called for the appropriate index paths.


Note that there is PromiseKit for Swift, which you may or may not find familiar.

How to wait for an API request to finish before storing data from function callback?

First you need to call completion when all of your data is loaded. In your case you call completion(tracksArray) before any of the getSavedTracks return.

For this part I suggest you to recursively accumulate tracks by going through all pages. There are multiple better tools to do so but I will give a raw example of it:

class TracksModel {

static func fetchAllPages(completion: @escaping ((_ tracks: [Track]?) -> Void)) {
var offset: Int = 0
let limit: Int = 50
var allTracks: [Track] = []

func appendPage() {
fetchSavedMusicPage(offset: offset, limit: limit) { tracks in
guard let tracks = tracks else {
completion(allTracks) // Most likely an error should be handled here
return
}
if tracks.count < limit {
// This was the last page because we got less than limit (50) tracks
completion(allTracks+tracks)
} else {
// Expecting another page to be loaded
offset += limit // Next page
allTracks += tracks
appendPage() // Recursively call for next page
}
}
}

appendPage() // Load first page

}

private static func fetchSavedMusicPage(offset: Int, limit: Int, completion: @escaping ((_ tracks: [Track]?) -> Void)) {
APICaller.shared.getUsersSavedTracks(limit: limit, offset: offset) { result in
switch result {
case .success(let model):
completion(model)
case .failure(let error):
print(error)
completion(nil) // Error also needs to call a completion
}
}
}

}

I hope comments will clear some things out. But the point being is that I nested an appendPage function which is called recursively until server stops sending data. In the end either an error occurs or the last page returns fewer tracks than provided limit.
Naturally it would be nicer to also forward an error but I did not include it for simplicity.

In any case you can now anywhere TracksModel.fetchAllPages { } and receive all tracks.

When you load and show your data (createSpinnerView) you also need to wait for data to be received before continuing. For instance:

func createSpinnerView() {

let loadViewController = LoadViewController.instantiateFromAppStoryboard(appStoryboard: .OrganizeScreen)
add(asChildViewController: loadViewController)

TracksModel.fetchAllPages { tracks in
DispatchQueue.main.async {
self.tracksArray = tracks
self.remove(asChildViewController: loadViewController)
self.navigateToFilterScreen(tracksArray: tracks)
}
}

}

A few components may have been removed but I hope you see the point. The method should be called on main thread already. But you are unsure what thread the API call returned on. So you need to use DispatchQueue.main.async within the completion closure, not outside of it. And also call to navigate within this closure because this is when things are actually complete.

Adding situation for fixed number of requests

For fixed number of requests you can do all your requests in parallel. You already did that in your code.
The biggest problem is that you can not guarantee that responses will come back in same order than your requests started. For instance if you perform two request A and B it can easily happen due to networking or any other reason that B will return before A. So you need to be a bit more sneaky. Look at the following code:

private func loadPage(pageIndex: Int, perPage: Int, completion: @escaping ((_ items: [Any]?, _ error: Error?) -> Void)) {
// TODO: logic here to return a page from server
completion(nil, nil)
}

func load(maximumNumberOfItems: Int, perPage: Int, completion: @escaping ((_ items: [Any], _ error: Error?) -> Void)) {
let pageStartIndicesToRetrieve: [Int] = {
var startIndex = 0
var toReturn: [Int] = []
while startIndex < maximumNumberOfItems {
toReturn.append(startIndex)
startIndex += perPage
}
return toReturn
}()

guard pageStartIndicesToRetrieve.isEmpty == false else {
// This happens if maximumNumberOfItems == 0
completion([], nil)
return
}

enum Response {
case success(items: [Any])
case failure(error: Error)
}

// Doing requests in parallel
// Note that responses may return in any order time-wise (we can not say that first page will come first, maybe the order will be [2, 1, 5, 3...])

var responses: [Response?] = .init(repeating: nil, count: pageStartIndicesToRetrieve.count) { // Start with all nil
didSet {
// Yes, Swift can do this :D How amazing!
guard responses.contains(where: { $0 == nil }) == false else {
// Still waiting for others to complete
return
}

let aggregatedResponse: (items: [Any], errors: [Error]) = responses.reduce((items: [], errors: [])) { partialResult, response in
switch response {
case .failure(let error): return (partialResult.items, partialResult.errors + [error])
case .success(let items): return (partialResult.items + [items], partialResult.errors)
case .none: return (partialResult.items, partialResult.errors)
}
}

let error: Error? = {
let errors = aggregatedResponse.errors
if errors.isEmpty {
return nil // No error
} else {
// There was an error.
return NSError(domain: "Something more meaningful", code: 500, userInfo: ["all_errors": errors]) // Or whatever you wish. Perhaps just "errors.first!"
}
}()

completion(aggregatedResponse.items, error)
}
}

pageStartIndicesToRetrieve.enumerated().forEach { requestIndex, startIndex in
loadPage(pageIndex: requestIndex, perPage: perPage) { items, error in
responses[requestIndex] = {
if let error = error {
return .failure(error: error)
} else {
return .success(items: items ?? [])
}
}()
}
}

}

The first method is not interesting. It just loads a single page. The second method now collects all the data.

First thing that happens is we calculate all possible requests. We need a start index and per-page. So the pageStartIndicesToRetrieve for case of 145 items using 50 per page will return [0, 50, 100]. (I later found out we only need count 3 in this case but that depends on the API, so let's stick with it). We expect 3 requests starting with item indices [0, 50, 100].

Next we create placeholders for our responses using

var responses: [Response?] = .init(repeating: nil, count: pageStartIndicesToRetrieve.count)

for our example of 145 items and using 50 per page this means it creates an array as [nil, nil, nil]. And when all of the values in this array turn to not-nil then all requests have returned and we can process all of the data. This is done by overriding the setter didSet for a local variable. I hope the content of it speaks for itself.

Now all that is left is to execute all requests at once and fill the array. Everything else should just resolve by itself.

The code is not the easiest and again; there are tools that can make things much easier. But for academical purposes I hope this approach explains what needs to be done to accomplish your task correctly.

How do I wait for an asynchronous call in Swift?


My issue is that my function is returning and execution continues before the API call is finished.

That's the whole point of asynchronous calls. A network call can take an arbitrary amount of time, so it kicks off the request in the background and tells you when it's finished.

Instead of returning a value from your code, take a callback parameter and call it when you know the Giphy call has finished. Or use a promise library. Or the delegate pattern.

The one thing that worked was a semaphore, but I think I remember reading that it wasn't best practice?

Don't do this. It will block your UI until the network call completes. Since you don't know how long that will take, your UI will be unresponsive for an unknown amount of time. Users will think your app has crashed on slow connections.

How to wait until data from network call comes and only then return value of a function #Swift

I think that you were on the right path when attempting to use a completion block, just didn't do it correctly.

func loadList(at page: Int, completion: @escaping ((Error?, Bool, [Beer]?) -> Void)) {
//MARK: - Checks is URL is valid + pagination
guard let url = URL(string: "https://api.punkapi.com/v2/beers?page=\(page)&per_page=25") else {
print("Invalid URL")
completion(nil, false, nil)
return
}
//MARK: - Creating URLSession DataTask
let task = URLSession.shared.dataTask(with: url){ data, response, error in
//MARK: - Handling no erros came
if let error = error {
completion(error, false, nil)
print(error!)
return
}
//MARK: - Handling data came
guard let data = data, let beers = try? JSONDecoder().decode([Beer].self, from: data) else {
completion(nil, false, nil)
return
}
completion(nil, true, beers)
}
task.resume()
}

This is the loadList function, which now has a completion parameter that will have three parameters, respectively the optional Error, the Bool value representing success or failure of obtaining the data, and the actual [Beers] array, containing the data (if any was retrieved).

Here's how you would now call the function:

service.loadList(at: page) { error, success, beers in
if let error = error {
// Handle the error here
return
}

if success, let beers = beers {
// Data was correctly retrieved - and safely unwrapped for good measure, do what you need with it
// Example:
loader.stopLoading()
self.datasource = beers
self.tableView.reloadData()
}
}

Bear in mind the fact that the completion is being executed asynchronously, without stopping the execution of the rest of your app.
Also, you should decide wether you want to handle the error directly inside the loadList function or inside the closure, and possibly remove the Error parameter if you handle it inside the function.
The same goes for the other parameters: you can decide to only have a closure that only has a [Beer] parameter and only call the closure if the data is correctly retrieved and converted.

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.

swift wait until the urlsession finished

Typically, when we want to know when a series of concurrent tasks (such as these network requests) are done, we would reach for a DispatchGroup. Call enter before the network request, call leave in the completion handler, and specify a notify block, e.g.

/// Load images
///
/// - Parameter completion: Completion handler to return array of URLs. Called on main queue

func loadImages(completion: @escaping ([URL]) -> Void) {
var imageURLs: [Int: URL] = [:] // note, storing results in local variable, avoiding need to synchronize with property
let group = DispatchGroup()
let count = 11

for index in 0..<count {
let url = URL(string: "https://foodish-api.herokuapp.com/api/")!
group.enter()
URLSession.shared.dataTask(with: url) { data, response, error in
defer { group.leave() }

guard let data = data else { return }

do {
let foodImage = try JSONDecoder().decode(FoodImage.self, from: data)
imageURLs[index] = foodImage.url
} catch {
print("JSON error: \(error.localizedDescription)")
}
}.resume()
}

group.notify(queue: .main) {
let sortedURLs = (0..<count).compactMap { imageURLs[$0] }
completion(sortedURLs)
}
}

Personally, rather than JSONSerialization, I use JSONDecoder with a Decodable type to parse the JSON response. (Also, I find the key name, image, to be a bit misleading, so I renamed it to url to avoid confusion, to make it clear it is a URL for the image, not the image itself.) Thus:

struct FoodImage: Decodable {
let url: URL

enum CodingKeys: String, CodingKey {
case url = "image"
}
}

Also note that the above is not updating properties or reloading the collection view. A routine that is performing network requests should not also be updating the model or the UI. I would leave this in the hands of the caller, e.g.,

var imageURLs: [URL]?

override func viewDidLoad() {
super.viewDidLoad()

// caller will update model and UI

loadImages { [weak self] imageURLs in
self?.imageURLs = imageURLs
self?.collectionView.reloadData()
}
}

Note:

  1. The DispatchQueue.main.async is not necessary. These requests already run asynchronously.

  2. Store the temporary results in a local variable. (And because URLSession uses a serial queue, we do not have to worry about further synchronization.)

  3. The dispatch group notify block, though, uses the .main queue, so that the caller can conveniently update properties and UI directly.

  4. Probably obvious, but I am parsing the URL directly, rather than parsing a string and converting that to a URL.

  5. When fetching results concurrently, you have no assurances regarding the order in which they will complete. So, one will often capture the results in some order-independent structure (such as a dictionary) and then sort the results before passing it back.

    In this particular case, the order doesn't strictly matter, but I included this sort-before-return pattern in my above example, as it is generally the desired behavior.

Anyway, that yields:

Sample Image

Swift- Waiting for asynchronous for-in loop to complete before calling completion handler swift

A couple of thoughts:

  1. Avoid ever calling wait from the main thread. The use cases for that are pretty limited. The notify is a much safer way to achieve the same thing.

  2. Make sure you call leave from every path inside you’re loop. This can be achieved nicely with defer block.

So:

func foo(completion: @escaping (Double?) -> Void) {
ref.observeSingleEvent(of: .value) { snapshot in
guard let bills = snapshot.value as? [String: AnyObject] else {
//error
completion(nil)
return
}

let group = DispatchGroup()
var runningTotal = 0.0

for billId in bills.keys {
group.enter()
print("Entering")
Database.database().reference().child("bills").child(billId).observeSingleEvent(of: .value) { snapshot in
defer { group.leave() }
guard let bill = snapshot.value as? [String: AnyObject] else {
return
}
if let amount = bill["amount"] as? Double {
runningTotal += amount
}
print("Leaving")
}
}
group.notify(queue: .main) {
completion(runningTotal)
}
}
}

How to pause an asynchronous Swift function until a callback is called when a result is available from another library(JavaScriptCore)

Okay, as it turns out, Swift async/await concurrency does have a support for continuation. It's not mentioned on the main article on Swift documentation and most 3rd party articles bill this feature as a way to integrate the old callback centric concurrency with the new async/await API, however this is much more useful than simply integrating the old code with the new one.

Continuation provides you with an asynchronous function that can be called from within your async function to pause execution with await until you explicitly resume it. That means, you can await input from the user or another library that uses callback to deliver the results. That also means, you are entirely responsible to correctly resume the function.

In my case, I'm providing the JavaScriptCore library with a callback that resumes execution from Swift, which is called by an async JavaScript code upon completion.

Let me show you. This is a JS code that asynchronously processes a request:

async function processRequest(data, callback){
try {
let result = await myAsyncJSProcessing(data)
callback(result, null)
} catch(err) {
callback(null, err)
}
}

To be able to pause the Swift execution until the JS code finishes processing and continue once the JS calls the callback with the data, I can use continuation like this:

//proccessRequestSync is a synchronous Swift function that provides the callback that the JS will call.
// This is also the function with a callback that Swift will wait until the callback is called
// self.engine is the initiated JavaScriptCore context where the JS runs.
// We create the callback that we will pass to the JS engine as JSValue, then pass it to the JS function as a parameter. Once the JS is done, calls this function.
func proccessRequestSync(_ completion : @escaping (Result<JSValue, Error>) -> Void){
let callback : @convention(block) (JSValue, JSValue) -> Void = { success, failure in
completion(.success(success))
}
let jsCallback = JSValue(object: callback, in: self.engine)
self.engine?.objectForKeyedSubscript("processRequest").call(withArguments: ["some_data", jsCallback])
}
// Then we create the async function that will be paused using continuation.
// We can integrate the function into the rest of our async Swift.
func proccessRequestAsync() async throws -> JSValue {
//Continuation happens right here.
return try await withCheckedThrowingContinuation({ continuation in
proccessRequestSync { result in
// This is the closure that will be called by the JS once processing is finished
// We will explicitly resume the execution of the Swift code by calling continuation.resume()
switch result {
case .success(let val):
continuation.resume(returning: val)
case .failure(let error):
continuation.resume(throwing: error)
}
}
})
}

if let result = try? await proccessRequestAsync()


Related Topics



Leave a reply



Submit