How to Wait for a Function to End on iOS/Swift, Before Starting the Second One

How to wait for a function to end on iOS/Swift, before starting the second one

Okay I've found an solution. I basically called the function at the end of the first one.

So basically:

    var searchLocation = String()
var searchLocationCoordinates = [String:Double]()
var searchRange = Int()

override func viewDidLoad() {
super.viewDidLoad()

// Gets the user's search preference
getTheSearchLocationAndRange()
}

func getTheSearchLocationAndRange() {
// Code for getTheSearchLocationAndRange()

loadDataFromDatabase()
}

func loadDataFromDatabase(){
// Code for loadDataFromDatabase()
}

How to wait for a function to complete in swift

Your third function depends on data from the first two, but the first two are independent.

You can use a DispatchGroup to execute the third function once the first two are complete. It doesn't matter in which order the first two functions complete.

You do need to consider that one or more of the network operations may fail.

Since these network operations are asynchronous, you will need to pass a completion handler to fetchDistance and invoke it when the work is done.

Something like


enum MyError: Error {
case locationNotFound
}

func fetchDistance(fromAdress: String, toAdress: String, completion: @escaping (Result<Double, Error>) -> Void) {

var firstLocation: CLCoordinate2D?
var secondLocation: CLLocationCoordinate2D?
var error: Error?

let dispatchGroup = DispatchGroup()

dispatchGroup.enter();

self.fetchLocation(for: fromAddress) { result in
switch result {
case .failure(let operationError):
operationError = operationError;
case .success(let location):
firstLocation = location
}

dispatchGroup.leave()

}

dispatchGroup.enter();

self.fetchLocation(for: toAddress) { result in
switch result {
case .failure(let operationError):
operationError = operationError;
case .success(let location):
secondLocation = location
}

dispatchGroup.leave()

}

dispatchGroup.notify() {
guard error != nil else {
completion(.failure(error))
return
}

guard let first = firstLocation, let second = secondLocation else {
completion(.failure(MyError.locationNotFound)
return
}

self.findDistance(from: first, to: second) { result in
completion(result)
}
}
}

func fetchLocation(for address: string, completion: completion: @escaping (Result<CLLocationCoordinate2D, Error>) -> Void) {

// Perform API call to get lat & Lon for address.
// Convert lat & lon to CLLocationCoordinate2D
// Invoke completion
/// completion(.success(location))
}

func findDistance(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D ,completion: completion: @escaping (Result<CLLocationCoordinate2D, Error>) -> Void) {

// Perform API call to get distance as double
// Invoke completion
/// completion(.success(distance))
}

wait for two asynchronous completion functions to finish, before executing next line code

Generally you'd use a dispatch group, enter the group before each asynchronous method, leave the group upon completion of each asynchronous method, and then set up a group notification when all "enter" calls are matched by corresponding "leave" calls:

override func viewDidLoad() {
super.viewDidLoad()

let group = dispatch_group_create()

dispatch_group_enter(group)
Females_NonChat() {
dispatch_group_leave(group)
}

dispatch_group_enter(group)
males_NonChat() {
dispatch_group_leave(group)
}

dispatch_group_notify(group, dispatch_get_main_queue()) {
print("finished executing both asynchronous functions")
}
}

func Females_NonChat(completionHandler: () -> ()) {
Anon_Ref.child("Chatting").child("female").observeSingleEventOfType(.Value) { snapshot in
if let FemInChatting = snapshot.value as? [String : String] {
print("executing")
}
completionHandler()
}
}

func males_NonChat(completionHandler: () -> ()) {
Anon_Ref.child("Chatting").child("male").observeSingleEventOfType(.Value) { snapshot in
print("executing")
completionHandler()
}
}

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 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.

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.

Waiting until the task finishes

If you need to hide the asynchronous nature of myFunction from the caller, use DispatchGroups to achieve this. Otherwise, use a completion block. Find samples for both below.


DispatchGroup Sample

You can either get notified when the group's enter() and leave() calls are balanced:

func myFunction() {
var a = 0

let group = DispatchGroup()
group.enter()

DispatchQueue.main.async {
a = 1
group.leave()
}

// does not wait. But the code in notify() is executed
// after enter() and leave() calls are balanced

group.notify(queue: .main) {
print(a)
}
}

or you can wait:

func myFunction() {
var a = 0

let group = DispatchGroup()
group.enter()

// avoid deadlocks by not using .main queue here
DispatchQueue.global(qos: .default).async {
a = 1
group.leave()
}

// wait ...
group.wait()

print(a) // you could also `return a` here
}

Note: group.wait() blocks the current queue (probably the main queue in your case), so you have to dispatch.async on another queue (like in the above sample code) to avoid a deadlock.


Completion Block Sample

func myFunction(completion: @escaping (Int)->()) {
var a = 0

DispatchQueue.main.async {
let b: Int = 1
a = b
completion(a) // call completion after you have the result
}
}


// on caller side:
myFunction { result in
print("result: \(result)")
}


Related Topics



Leave a reply



Submit