This question already has answers here:
DispatchGroup logical workflow
(2 answers)
Closed 1 year ago.
I have two methods in the completeOnboarding method and both of them have network operation which should be done in the background thread as follows. However, I am wondering if I am doing why completion(true) gets called first, how could I able to handle that issue?
DispatchQueue.global(qos: .background).async {
self?.completeOnboarding( completion: { (success) in
DispatchQueue.main.async {
if success {
print("success")
} else {
print("failed")
}
}
})
func completeOnboarding(completion: #escaping(Bool) -> Void){
// has network post operation
classRegistration() {(success) in
if !success {
completion(false)
return
}
}
// has network post operation
classLocation() { (success) in
if !success {
completion(false)
return
}
}
completion(true)
}
The final completion(true) is not waiting for classLocation() and classRegistration() calls. If you have multiple network calls and you want to wait for all of them to finish you could (one approach) add them to a DispatchGroup and wait for that one to finish:
func dispatchAndWait(completion: #escaping () -> Void) {
func networkOne(completion: #escaping (_ success: Bool) -> Void) {
print("[DEBUG] Enter \(#function)")
DispatchQueue.main.asyncAfter(deadline: .now() + Double.random(in: 0...3)) {
completion(Bool.random())
}
print("[DEBUG] Return \(#function)")
}
func networkTwo(completion: #escaping (_ success: Bool) -> Void) {
print("[DEBUG] Enter \(#function)")
DispatchQueue.main.asyncAfter(deadline: .now() + Double.random(in: 0...3)) {
completion(Bool.random())
}
print("[DEBUG] Return \(#function)")
}
// Create a DispatchGroup and add both calls
let dispatchGroup = DispatchGroup()
// Enter first network call
dispatchGroup.enter()
networkOne { success in
print("[DEBUG] Complete networkOne with success: \(success)")
// Exit first network call
dispatchGroup.leave()
}
// Enter second network call
dispatchGroup.enter()
networkTwo { success in
print("[DEBUG] Complete networkTwo with success: \(success)")
// Exit second network call
dispatchGroup.leave()
}
// notify gets called when all tasks have exited
dispatchGroup.notify(queue: DispatchQueue.main) {
completion()
}
}
One more advise:
classRegistration() {(success) in
if !success {
completion(false)
return
}
}
will never complete in case of success==true, you should make sure that the completion is called on every path
classRegistration() {(success) in
// ... do whatever needs to be done here
completion(success)
}
Assuming classRegistration needs to succeed for classLocation to begin --
quick & dirty --
func completeOnboarding(completion: #escaping(Bool) -> Void){
// has network post operation
classRegistration() {(success) in
if success {
// has network post operation
classLocation() { (success) in
completion(success)
}
} else {
completion(false)
}
}
}
a proper way (others include - NSOperations with dependency, Dispatch group)
func completeOnboarding(completion: #escaping(Bool) -> Void){
let serialQueue = DispatchQueue(label: "classname.serial")
var proceedWithSuccess = true
serialQueue.async {
serialQueue.suspend() //run 1 operation at a time
classRegistration() {(success) in
proceedWithSuccess = success
serialQueue.resume() //let next operation run
}
}
serialQueue.async {
guard proceedWithSuccess else { return }
serialQueue.suspend()
classLocation() { (success) in
proceedWithSuccess = success
serialQueue.resume()
}
}
serialQueue.async {
completion(proceedWithSuccess)
}
}
If you want classLocation() to fire even if registration fails - just get rid of guard statement above.
If it were up to me I'd use a custom NSOperation subclass for Async operation & explicitly mention dependency between operations but it needs a ton of boilerplate code (perhaps something to look into later); serial queue (or dispatch group from the other answer) oughta be enough for you in this case though.
Related
In the Button action, I call getLatLongValues method, It will return completion property after API success.
Hear problem is, If I click N th times in button action, getLatLongValues method execute N times.
Like I click the button in 1st time getLatLongValues execute 1 time, I'm click 2nd time not two times getLatLongValues method execute 2 times.
#IBAction func updateDeliveryAddress() {
guard let address = self.addressTextField.text else { return }
self.getLatLongValues(address, true, viewModel) { success in
if success {
//Success
} else {
// Error
}
}
}
func getLatLongValues(address: String, setAsDefault: Bool, viewModel:ViewModel, completion: #escaping (_ success: Bool) -> Void) {
viewModel.location.subscribe(onNext: { [weak self] results in
guard self != nil else { return }
if let result = results {
completion(true) // Success
}
}).disposed(by: disposeBag)
viewModel.fetchLocation(address: address)
}
Why getLatLongValues Execute N times?
because each time you are creating a new subscription.
a subscription is not a completion handler that gets executed once.
viewModel.location.subscribe(onNext: { [weak self] results in
should only be called once, and on each location update, you get the result in the block
If you want the completion handler to only be called once, you should set a flag:
var completionHandlerExecuted = false /// false at first
func getLatLongValues(address: String, setAsDefault: Bool, viewModel:ViewModel, completion: #escaping (_ success: Bool) -> Void) {
if completionHandlerExecuted == false {
completionHandlerExecuted = true /// set to true, so it won't be called again
viewModel.location.subscribe(onNext: { [weak self] results in
guard self != nil else { return }
if let result = results {
completion(true) // Success
}
}).disposed(by: disposeBag)
viewModel.fetchLocation(address: address)
}
}
Completion handlers are like any other instruction you put in your functions.
Every time getLatLongValues is called, you are doing a subscribe, which will call the completion handler once it's finished.
In case it matters, this app was a 4.2 app but is upgrading to 5.0 with new functionality, including this.
In response to a content-available APN, I need to combine local device data with remote data before triggering a message to a third party. In the foreground, this process works, but in the background, it appears to freeze until the app is in the foreground.
I thought to resolve this with a DispatchQueue -- and that is getting me a bit further , but it is still not going all the way through.
When I receive my APN, I ensure it looks right (its a content-avaialbe notification and has a category), then fire off loadPrediction:
// Tells the app that a remote notification arrived that indicates there is data to be fetched.
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler:
#escaping (UIBackgroundFetchResult) -> Void) {
guard let aps = userInfo["aps"] as? [String: AnyObject] else {
completionHandler(.failed)
return
}
if aps["content-available"] as? Int == 1 {
guard let category = aps["category"] as? String else {
print("on didReceiveRemoteNotification - did not receive payload with category")
print(String(describing: userInfo))
return
}
switch category {
case APNCATEGORIES.PREDICTION.rawValue:
DataModel.shared.loadPredictions() {
completionHandler(.newData)
}
break
default:
print("on didReceiveRemoteNotification - received unknown category '\(category)'")
completionHandler(.failed)
}
} else {
print("on didReceiveRemoteNotification - did not receive content-available in APN")
print(String(describing: aps))
completionHandler(.noData)
}
}
In loadPredictions, I request two pieces of data from the backend. edit: I've read that you might want to start a different queue for each POST request, so I've revised this next code block to its current form instead of just one queue:
/** load prediction data for notification scheduling */
func loadPredictions(_ callback: #escaping () -> Void) {
print("loading predictions")
let queue = DispatchQueue(label: "loadingPredictions", qos: .utility, attributes: .concurrent)
queue.sync { [weak self] in
print("loading predictions - in async task, about to getPredictionsDataFromFirestore")
self?.getPredictionsDataFromFirestore() { [weak self] in
print("getting Predictions Data from Firestore")
if let error = $2 {
NotificationCenter.default.post(Notification(name: DataModel.constants.dataFailedToLoad, object: error))
} else {
let apps = $0
apps.forEach { app in
print("for each app - about to getNotificationSpecificationFromFireStore")
let queue = DispatchQueue(label: "getNotificationSpecificationFromFireStore_\(app.name)", qos: .utility, attributes: .concurrent)
queue.async { [weak self] in
print("getting Notification Specification from FireStore")
self?.getNotificationSpecificationFromFireStore(app: app) { [weak self] spec, error in
print("got Notification Specification from FireStore, about to post notification")
if(error != nil) {
return
}
guard let spec = spec else {
return
}
self?.postNotification(app: app, spec: spec)
}
}
}
// loadMergedForecasts($1)
NotificationCenter.default.post(Notification(name: DataModel.constants.predictionsDataLoaded))
}
callback()
}
}
}
They don't really need to be dependently related like that, but there's no point in doing the second one if the first fails.. If they both succeed, I should post a notification to my recipient in postNotification:
/** notify third party app of available notificatiions to schedule */
func postNotification (app: App, spec: NotificationSpecification) {
print("posting notification")
do {
let notify = Data(app.notify.utf8)
let localNotificationDetails = try JSONDecoder().decode(NotificationDetails.self, from: notify)
if spec.p8 != "custom" {
let token = localNotificationDetails.token
} else {
guard let bodyJSON = localNotificationDetails.body else {
return
}
guard let url = spec.custom_endpoint else { return }
guard let methodString = spec.custom_method?.uppercased() else { return }
guard let method = HTTPMethod(rawValue:methodString) else { return }
if ![.post, .put, .patch].contains(method) {
print("app has unsupported method '\(method)' -- \(String(describing: app))")
return
}
guard var headers = spec.custom_headers else { return }
if !headers.keys.map({ entry_key in entry_key.uppercased() }).contains("CONTENT-TYPE") {
headers["Content-Type"] = "application/json"
}
print("manually posting the notification with \(String(describing: bodyJSON))")
let queue = DispatchQueue(label: "manuallyPostNotifications", qos: .utility, attributes: .concurrent)
AF.request(url, method:method, parameters: bodyJSON).responseJSON(queue: queue) { response in
switch response.result {
case .success:
print("Validation Successful")
case .failure(let error):
print(error)
}
}
}
} catch let e {
print("error posting notification to app \(app.id)\n\(e)")
}
}
NONE of these methods are on a View.
At first, there were zero cues and I dont know that I made it past the first loadPrediction. In its current state, the log looks like this when the app was in the background:
loading predictions
loading predictions - in async task, about to getPredictionsDataFromFirestore
getting Predictions Data from Firestore
for each app - about to getNotificationSpecificationFromFireStore
getting Notification Specification from FireStore
edit: that's one additional line, but it doesnt represent any improvement for the additional queues.
It will complete and succeed if I foreground it (and he whole thing takes 1-2 seconds when fully in the foreground). But I'd like to do all my work now.
Questions:
I'm doing queues wrong. How do I not exhaust the queue that I am in?
Can anyone confirm or deny that this will work when the app is closed? I can see that work is done when the app is closed, but I haven't since gone back to test if the api calls work because I cant get it to work just in the background.
addendum
revised code for current answer
/** load prediction data for notification scheduling */
func loadPredictions(_ callback: #escaping () -> Void) {
print("loading predictions")
let queue = DispatchQueue(label: "loadingPredictions", qos: .default)
queue.sync { [weak self] in
let group = DispatchGroup()
group.enter()
print("loading predictions - in async task, about to getPredictionsDataFromFirestore")
self?.getPredictionsDataFromFirestore() { [weak self] in
print("getting Predictions Data from Firestore")
if let error = $2 {
NotificationCenter.default.post(Notification(name: DataModel.constants.dataFailedToLoad, object: error))
} else {
let apps = $0
apps.forEach { app in
print("for each app - about to getNotificationSpecificationFromFireStore")
group.enter()
print("getting Notification Specification from FireStore")
self?.getNotificationSpecificationFromFireStore(app: app) { [weak self] spec, error in
print("got Notification Specification from FireStore, about to post notification")
if(error != nil) {
group.leave()
return
}
guard let spec = spec else {
group.leave()
return
}
self?.postNotification(app: app, spec: spec) {
group.leave()
}
}
group.leave()
}
// loadMergedForecasts($1)
NotificationCenter.default.post(Notification(name: DataModel.constants.predictionsDataLoaded))
group.leave()
}
group.notify(queue: .main) {
callback()
print("I am being called too early?")
}
}
}
}
and (added a callback to the final method call):
/** notify third party app of available notificatiions to schedule */
func postNotification (app: App, spec: NotificationSpecification, _ callback: #escaping () -> Void ) {
print("posting notification")
do {
let notify = Data(app.notify.utf8)
let localNotificationDetails = try JSONDecoder().decode(NotificationDetails.self, from: notify)
if spec.p8 != "custom" {
let token = localNotificationDetails.token
callback()
} else {
guard let bodyJSON = localNotificationDetails.body else {
callback()
return
}
guard let url = spec.custom_endpoint else {
callback()
return
}
guard let methodString = spec.custom_method?.uppercased() else {
callback()
return
}
guard let method = HTTPMethod(rawValue:methodString) else {
callback()
return
}
if ![.post, .put, .patch].contains(method) {
print("app has unsupported method '\(method)' -- \(String(describing: app))")
callback()
return
}
guard var headers = spec.custom_headers else { return }
if !headers.keys.map({ entry_key in entry_key.uppercased() }).contains("CONTENT-TYPE") {
headers["Content-Type"] = "application/json"
}
print("manually posting the notification with \(String(describing: bodyJSON))")
let queue = DispatchQueue(label: "manuallyPostNotifications", qos: .utility, attributes: .concurrent)
AF.request(url, method:method, parameters: bodyJSON).responseJSON(queue: queue) { response in
switch response.result {
case .success:
print("Validation Successful")
case .failure(let error):
print(error)
}
callback()
}
}
} catch let e {
print("error posting notification to app \(app.id)\n\(e)")
callback()
}
}
Realizing that my print statement wasn't inside the notify callback, I've revised it -- still not getting inside of the second firebase call.
loading predictions
loading predictions - in async task, about to getPredictionsDataFromFirestore
getting Predictions Data from Firestore
for each app - about to getNotificationSpecificationFromFireStore
getting Notification Specification from FireStore
I am being called too early?
You are firing off asynchronous tasks and your callback() will be executed before these tasks are complete. Since callback() eventually calls the completionHandler, your app will be suspended before all of its work is done.
You can use a dispatch group to delay the callBack() until everything is complete. The additional dispatch queues aren't necessary.
func loadPredictions(_ callback: #escaping () -> Void) {
print("loading predictions")
let dispatchGroup = DispatchGroup()
print("loading predictions - in async task, about to getPredictionsDataFromFirestore")
dispatchGroup.enter()
self.getPredictionsDataFromFirestore() {
print("getting Predictions Data from Firestore")
if let error = $2 {
NotificationCenter.default.post(Notification(name: DataModel.constants.dataFailedToLoad, object: error))
} else {
let apps = $0
apps.forEach { app in
print("for each app - about to getNotificationSpecificationFromFireStore")
dispatchGroup.enter()
self.getNotificationSpecificationFromFireStore(app: app) { spec, error in
print("got Notification Specification from FireStore, about to post notification")
if(error != nil) {
dispatchGroup.leave()
return
}
guard let spec = spec else {
dispatchGroup.leave()
return
}
self.postNotification(app: app, spec: spec)
dispatchGroup.leave()
}
}
}
NotificationCenter.default.post(Notification(name: DataModel.constants.predictionsDataLoaded))
dispatchGroup.leave()
}
dispatchGroup.notify(queue: .main) {
callback()
}
}
I am trying to call 3 functions in order but each function needs to have been completed before the next should run. Each function has a completion handler that calls another function upon completion. After reading lots online about dispatch queues I though this may be the best way to approach it, that's if I am understanding it correctly of course. When I run my code Each function is called in order but not when the previous has been completed. In the first function I am downloading an image from firebase but the second function gets called before the image has downloaded. I've taken out specifics in my code but this is what I have so far.
typealias COMPLETION = () -> ()
let functionOne_completion = {
print("functionOne COMPLETED")
}
let functionTwo_completion = {
print("functionTwo COMPLETED")
}
let functionThree_completion = {
print("functionThree COMPLETED")
}
override func viewDidLoad() {
super.viewDidLoad()
let queue = DispatchQueue(label: "com.myApp.myQueue")
queue.sync {
functionOne(completion: functionOne_completion)
functionTwo(completion: functionTwo_completion)
functionThree(completion: functionThree_completion)
}
func functionOne(completion: #escaping COMPLETION) {
print("functionOne STARTED")
completion()
}
func functionTwo(completion: #escaping COMPLETION) {
print("functionTwo STARTED")
completion()
}
func functionThree(completion: #escaping COMPLETION) {
print("functionThree STARTED")
completion()
}
You could use DispatchGroup
DispatchQueue.global().async {
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
functionOne { dispatchGroup.leave() }
dispatchGroup.wait() //Add reasonable timeout
dispatchGroup.enter()
functionTwo { dispatchGroup.leave() }
dispatchGroup.wait()
dispatchGroup.enter()
functionThree { dispatchGroup.leave() }
dispatchGroup.wait()
dispatchGroup.notify(queue: .main) {
//All tasks are completed
}
}
You need to call the second function on the completion of the first.
Something like:
func first(_ completion : #escaping()->()){
print("first")
completion()
}
func second(_ completion : #escaping()->()){
print("second")
}
func third(){
print("third")
}
override func viewDidLoad(){
....
first{
self.second{
self.third()
}
}
}
So when your image download gets finished, inside the completion block where you get the callback of download completion, you should call your second method/block passed as argument which in turn will call your second method.
In my example code below, I call complete(false) on failure. However, since I'm using a DispatchGroup object to make sure all asynchronous requests are complete, I cannot just call syncGroup.leave() on failure, as the notify will be called, which contains complete(true), making this function return true, when it should be returning false for failure.
Am I correct in not calling syncGroup.leave() on failure to complete my function correctly? Or should I be calling syncGroup.leave() and somehow trying to determine what the result is, so I can return false on failure?
let syncGroup = DispatchGroup()
syncGroup.enter()
for track in unsynced {
register(time: time, withCompletion: { (success: Bool) -> () in
if success {
self.debug.log(tag: "SyncController", content: "Registered")
syncGroup.leave()
}
else {
complete(false)
}
})
}
//all requests complete
syncGroup.notify(queue: .main) {
self.debug.log(tag: "SyncController", content: "Finished registering")
complete(true)
}
You have to enter the group within your for loop. You might want to introduce an additional error flag.
Example implementation:
var fail = false
let syncGroup = DispatchGroup()
for track in unsynced {
syncGroup.enter()
register(time: time, withCompletion: { (success: Bool) -> () in
if success {
self.debug.log(tag: "SyncController", content: "Registered")
syncGroup.leave()
}
else {
fail = true
syncGroup.leave()
}
})
}
//all requests complete
syncGroup.notify(queue: .main) {
if fail {
complete(false)
} else {
self.debug.log(tag: "SyncController", content: "Finished registering")
complete(true)
}
}
So I have situation when I've want do some async stuff, post status on Facebook and get notified when it's done posting. I was hoping that dispatch_group_async will do the job but now I hit the wall.
Here is how at this moment logic look like.
Func
func saveAndPost() {
//1. save
dispatch_group_async(group, queue) { () -> Void in
print("saving to DB")
//some func
print("saved to DB")
}
//2. post
dispatch_group_async(group, queue, { () -> Void in
print("publishing on FB")
//some func
//async request to ACAccount {
print("access")
//async performRequestWithHandler{
print("posted")
//anwser from request
}
}
}
print("published on FB")
})
dispatch_group_notify(group, queue, { () -> Void in
print("done")
dispatch_async(dispatch_get_main_queue(), { () -> Void in
//go to next view
})
})
}
Output
saving to DB
saved to DB
publishing on FB
published on FB
done
post
posted
My goal is to get notified when whole posting process is done. Maybe this is not possible with dispatch_group_async and I have to use KVO or even PromiseKit?
SOLUTION
David got me thinking and dispatch_group_enter & 'dispatch_group_leave' right what I needed. So at this moment my logic look like this.
func saveAndPost() {
//1. save
dispatch_group_async(group, queue) { () -> Void in
print("saving to DB")
//some func
print("saved to DB")
}
//2. post
dispatch_group_enter(group)
print("publishing on FB")
//some func
//async request to ACAccount {
print("access")
//async performRequestWithHandler{
print("posted")
//anwser from request
dispatch_group_leave(group)
}
}
}
print("published on FB")
dispatch_group_notify(group, queue, { () -> Void in
print("done")
dispatch_async(dispatch_get_main_queue(), { () -> Void in
//go to next view
})
})
}
As you can see part 2. is now modified.
My preferred method to tackle this when dealing with async calls:
var asyncGroup = dispatch_group_create()
dispatch_group_enter(asyncGroup)
asyncCall1.start() {
//end of callback
dispatch_group_leave(asyncGroup)
}
dispatch_group_enter(asyncGroup)
asyncCall2.start() {
asyncCall3.start() {
asyncCall4.start() {
dispatch_group_leave(asyncGroup)
}
}
}
dispatch_group_notify(asyncGroup, dispatch_get_main_queue(), {
println("done")
})
asyncCall1.start() { ... } and asyncCall2.start() { ... } are just sorta psuedocode to show you how it works.
Every dispatch_group_enter call needs to be balanced by a dispatch_group_leave call or you'll never get into the dispatch_group_notify block.