Run async tasks using Background Tasks

I've got an iOS app that performs a series of operations when initialized and when a refresh is performed.

In short, that app has:

  1. An obervable object (appManager) that houses variables that act as "state" for the app.

  2. Methods in the observable object that perform a variety of operations.

Here's an example:

  1. Perform an API operation to get tokens to perform API actions on remote API service.
  2. Perform a fetch on an external database to retrieve some data.
  3. Decode that data an place the data into @Published variables, using (DispatchQueue.main.async).
  4. Perform another remote API fetch operation on a different endpoint.
  5. Put more variables in "state"

So when the user opens the app and after init runs, they see what they expect to see. And when they tap a "refresh" button, another function (e.g. getUpdatedAppData()) is called with similar steps to the above, and the app updates the published variables and the view(s) update.

The above steps are all performed using async functions, using Swift's new async/await functionality. Everything works swimmingly when the app is in the foreground.

What I'd like to do is perform the updateAppData() function using Background tasks as described here..

I have successfully setup the app to register and schedule a task, using BGTaskScheduler, and I can successfully emulate the triggerring of that task by setting a breakpoint and running the following code in the console:

e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.example.task"]

If the background tasks is something simple, like printing to the console, it seems to work

   private func configureBackgroundTasks() {
    Logger.log(.info, "Registering background tasks...")
    let bgTaskIdentifier = "com.example.task"
    BGTaskScheduler.shared.register(forTaskWithIdentifier: bgTaskIdentifier, using: DispatchQueue.main) { (task) in
      Logger.log(.info, "Performing background task \(bgTaskIdentifier)")
      task.setTaskCompleted(success: true)
      backgroundTaskManager.scheduleAppRefresh()
    }
  }

However, if the function is something like the below, I can see the function being triggered, when simulating the running of the task, but the function hangs at the start of the first async operation (e.g. fetch tokens above).

It seems to "try to complete" (and usuually fails) once the app is brought to the foreground, but that's obviously not what I want to happen.

Surely, I'm misunderstanding something and/or doing something wrong, so I'm hoping to get some help here.

Thanks in advance for any assistance toward achieving what I'm trying to do here.

   private func configureBackgroundTasks() {
    Logger.log(.info, "Registering background tasks...")
    let bgTaskIdentifier = "com.example.task"
    BGTaskScheduler.shared.register(forTaskWithIdentifier: bgTaskIdentifier, using: DispatchQueue.main) { (task) in
      Logger.log(.info, "Performing background task \(bgTaskIdentifier)")
      Task.init {
        if let updatedData = await appManager.getUpdateAppData() {
          DispatchQueue.main.async {
            appManager.data = updatedData
          }
        }
      }
      task.setTaskCompleted(success: true)
      backgroundTaskManager.scheduleAppRefresh()
    }
  }

I'm misunderstanding something and/or doing something wrong

As far as I see the documentation of setTaskCompleted(success:), it should be called when the task is completed.

Have you tried something like this?

    private func configureBackgroundTasks() {
        Logger.log(.info, "Registering background tasks...")
        let bgTaskIdentifier = "com.example.task"
        BGTaskScheduler.shared.register(forTaskWithIdentifier: bgTaskIdentifier, using: DispatchQueue.main) { (task) in
            Logger.log(.info, "Performing background task \(bgTaskIdentifier)")
            Task {
                if let updatedData = await appManager.getUpdateAppData() {
                    DispatchQueue.main.async {
                        appManager.data = updatedData
                        task.setTaskCompleted(success: true)
                    }
                } else {
                    task.setTaskCompleted(success: false)
                }
                backgroundTaskManager.scheduleAppRefresh()
            }
        }
    }

Hi @OOPer,

Thanks much for taking the time to respond.

I have tried several variations, in an attempt to get this working.

The result from your suggested mods: using Task instead of Task.init and putting task.setTaskCompleted(success: true) in a DispatchQueue.main.async {}, results in:

2021-12-04 14:58:39.136393-0800 App[8341:2562502] [connection] nw_read_request_report [C7] Receive failed with error "Socket is not connected"
2021-12-04 14:58:39.139668-0800 App[8341:2562502] [connection] nw_read_request_report [C7] Receive failed with error "Socket is not connected"

Doing the same, but using Task.init {... results in:

2021-12-04 15:03:53.128265-0800 App[8344:2563988] [Framework] Task <BGAppRefreshTask: com.example.task> dealloc'd without completing. This is a programmer error.
2021-12-04 15:03:53.128610-0800 App[8344:2563988] Marking simulated task complete: (null)

I do like the very specific calling out the programmer error 😄

I've also tried using BGProcessingTaskRequest instead of BGAppRefreshTaskRequest just in case the update operation was timing out after 30 seconds, which I believe is the time window alloted for a refresh task.

OOPer’s suggestion is pretty much what I’d have said. Three things:

  • Task { … } and Task.init { … } are two ways to spell the same thing. If you’re seeing a difference between the two, there’s something wonky with your test methodology.

  • Unless the nw_read_request_report stuff is correlated with a failure, it’s most likely log noise.

So, other than the potential log noise, does anything else go wrong when you use OOPer’s approach?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

HI @eskimo,

Thanks for chiming in and providing insight.

Using the below, I set a breakpoint right when the app is moved to the background (I'm not using appDelegate).

.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
      backgroundTaskManager.scheduleAppRefresh() // <= breakpoint here
    }

Once the app is paused, in Xcode, I run the following command in the Xcode terminal:

e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.example.task"]

That responds with:

2021-12-07 05:29:25.405222+0700 App[602:49934] Simulating launch for task with identifier com.example.task

After that, I resume the app in Xcode and see (I believe "missing data" is not an error below):

2021-12-07 05:29:29.724492+0700 App[602:50205] Starting simulated task: <decode: missing data>

Followed by the initial operation of the getUpdateAppData() function being called.

The initial operation is a function that runs an Amplify (iOS library) graphQL query (here's an excerpt):

func getMyFriends(currentUser: User) async throws -> [Friend] {
    Logger.log(.fetch, "Fetching \(currentUser.username)'s friends...")
    return try await withCheckedThrowingContinuation { continuation in
      let friend = Friend.keys
      let predicate = friend.owner == currentUser.username
      Amplify.API.query(request: .list(Friend.self, where: predicate)) { event in
        switch event {
        case .success(let result):
...

The app seems to just stop right after the logging of "Fetching (currentUser.username)'s friends..."

No errors are thrown that I can see.

Note this function works fine when it's called when the app is in the foreground.

Also, when I return the app to the foreground, the getUpdateAppData() tries to resume, and rarely (if ever) completes as it should:

🐕 Fetching Elmer's friends...
2021-12-07 06:02:20.150514+0700 App[654:65556] Connection 1: encountered error(1:53) // <= upon return to foreground
2021-12-07 06:02:20.150650+0700 App[654:65556] Connection 1: received failure notification
2021-12-07 06:02:20.151257+0700 App[654:65556] [connection] nw_connection_copy_connected_local_endpoint [C1] Connection has no connected path
2021-12-07 06:02:20.151450+0700 App[654:65556] [connection] nw_connection_copy_connected_remote_endpoint [C1] Connection has no connected path
2021-12-07 06:02:20.153068+0700 App[654:65556] Connection 2: encountered error(1:53)
2021-12-07 06:02:20.153125+0700 App[654:65556] Connection 2: received failure notification
2021-12-07 06:02:20.153280+0700 App[654:65556] [connection] nw_connection_copy_connected_local_endpoint [C2] Connection has no connected path
2021-12-07 06:02:20.153891+0700 App[654:65556] [connection] nw_connection_copy_connected_remote_endpoint [C2] Connection has no connected path
2021-12-07 06:02:20.155082+0700 App[654:65556] Connection 3: encountered error(1:53)
2021-12-07 06:02:20.155293+0700 App[654:65556] Connection 3: received failure notification
2021-12-07 06:02:20.161795+0700 App[654:65556] [connection] nw_connection_copy_connected_local_endpoint [C3] Connection has no connected path
2021-12-07 06:02:20.161877+0700 App[654:65556] [connection] nw_connection_copy_connected_remote_endpoint [C3] Connection has no connected path
🦴 Successfully fetched Elmer's friends!
...

Maybe Amplify doesn't work when in the background? I dunno.

One thing that I have yet to try is testing with a simple, single API operation in lieu of the somewhat more involved getUpdateAppData() function that is currently running through completion.

Any further thoughts and/or advice is appreciated.

private func configureBackgroundTasks() {
        Logger.log(.info, "Registering background tasks...")
        let bgTaskIdentifier = "com.example.task"
        BGTaskScheduler.shared.register(forTaskWithIdentifier: bgTaskIdentifier, using: DispatchQueue.main) { (task) in
            Logger.log(.info, "Performing background task \(bgTaskIdentifier)")
            Task {
                if let updatedData = await appManager.getUpdateAppData() {
                    DispatchQueue.main.async {
                        appManager.data = updatedData
                        task.setTaskCompleted(success: true)
                    }
                } else {
                    task.setTaskCompleted(success: false)
                }
                backgroundTaskManager.scheduleAppRefresh()
            }
        }
    }

Thanks for the explanation.

Followed by the initial operation of the getUpdateAppData() function being called.

So, to be clear, you’re calling this from a background task registered using the code shown by OOPer in this post?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hi @eskimo,

Thanks for the pro follow up. That is correct.

To clarify further, getUpdateAppData() function is run after manually invoking the objc command in Xcode. I don't actually know of another way to test this.

The task is initially registered with the configureBackgroundTasks() function (see last code block in my prior post) in the init function of the app's app file (not app delegate).

Kindly let me know if you have any further questions.

Hmmm, I’m running out of ideas. The background task should prevent your app from being suspended until it’s completed, and that should allow your networking to work correctly.

If you set an expiry handler on the background task (via the expirationHandler property) does it get called?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hello @Kimfucious Did you manage to solve it?

I am also facing the similar issue.

Thanks

Run async tasks using Background Tasks
 
 
Q