0

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?
roberto tomás
  • 4,435
  • 5
  • 42
  • 71
  • What do this print statements mean? You use either `wait` or `notify`, not both. You can get rid of all of that weak self stuff because you can't create a retain cycle and you don't need the synchronous queue dispatch; the work will all be performed asynchronously anyway – Paulw11 Apr 18 '19 at 20:53
  • So, you're right, my first approach was not printing useful information once we got to the leave() calls -- but I believe I have it now, and it is definitely calling inside of the notify block :( – roberto tomás Apr 20 '19 at 01:19
  • and I now I got it.. thank you for your help! – roberto tomás Apr 20 '19 at 01:47

1 Answers1

1

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()
    }
}
Paulw11
  • 108,386
  • 14
  • 159
  • 186
  • thank you Paul, I am in a webinar but will implement this and accept later :) – roberto tomás Apr 18 '19 at 01:03
  • after adjusting the brackets , I am still noticing that the line `dispatchGroup.notify {` raises the error `Missing argument for parameter 'queue' in call` -- this makes me think I should be adding a queue at the instantiation of the group – roberto tomás Apr 18 '19 at 13:23
  • Sorry, yes, I typed it from (faulty) memory – Paulw11 Apr 18 '19 at 13:24
  • I've revised my code and will add it to the OP so you can verify -- but it is not progressing any further – roberto tomás Apr 18 '19 at 13:38
  • I am able to verify that you are correct (see the end of OP now) - but my implementation is not. – roberto tomás Apr 18 '19 at 13:50
  • I have fixed the {} in my original answer; I left some in when I removed the queuing. You need to ensure you call `dispatchGroup.enter()` before you start an asynchronous task and `leave()` before you exit the handler for that task. The number of calls to `enter` and `leave` must balance. `notify` executes when `leave()` has been called for all `enter()`s – Paulw11 Apr 18 '19 at 21:03