Periodic iOS background execution

An app has some periodic light background work, which is expected to run even when app is suspended.

Ofc, if user has swiped up, nothing can be done. When swiped from app switcher, if app was in Suspended state, its not informed else, in every other state (including background), applicationWillTerminate(_:) is invoked

When application is not killed by user and is in suspended state, I want to run some small tasks at certain intervals.

According to this documentation, Background fetch is the way to go. This post by Apple DTS also suggests the same i.e., Background Fetch (BGAppRefreshTask) for periodic background execution.

Example: The sample background task is to count 10s in multiple of 10s.

Launch handler is registered with the background task ID configured in info.plist file in application(_:didFinishLauchingWithOptions:). I have the following code invoked from in applicationDidEnterBackground(_:),

func initiateBackgroundTask(id: String) {

        let currentDate: Date = Date.now
        guard let futureDate: Date = Calendar.current.date(byAdding: .minute, value: 1, to: currentDate) else {
            NSLog(AppDelegate.TAG + "Error in obtaining future date!")
            return
        }

        let taskRequest: BGAppRefreshTaskRequest = BGAppRefreshTaskRequest(identifier: id)

        // Schedule background task
        taskRequest.earliestBeginDate = futureDate

        do {
            // Note: Background processing doesn't work in Simulator.
            // Submitting in Simulator leads to BGTaskScheduler.Error.Code.unavailable error.

            try BGTaskScheduler.shared.submit(taskRequest)
            NSLog(AppDelegate.TAG + "Task submitted successfully!")
        } catch {
            NSLog(AppDelegate.TAG + "error = " + String(describing: error))
            return
        }

        NSLog(AppDelegate.TAG + "Background task scheduled successfully!!")
        NSLog(AppDelegate.TAG + "Scheduled date = %@ | Present date = %@", String(describing: taskRequest.earliestBeginDate), String(describing: Date.now))

    }

The following method is invoked from launch handler:

func taskRefreshCount1Min(task: BGAppRefreshTask) {
     
        // Set expiration handler
        task.expirationHandler = {
           NSLog(AppDelegate.TAG + "Background execution time about to expire!")
           NSLog(AppDelegate.TAG + "Remaining time = %fsec | %@", AppDelegate.app.backgroundTimeRemaining, String(describing: Date.now))

            NSLog(AppDelegate.TAG + "Unable to complete task!!")
            task.setTaskCompleted(success: false)
        }

        for i in 1...6 {
            let presentDate: Date = Date.now
            NSLog(AppDelegate.TAG + "%d) %@", i, String(describing: presentDate))
            Thread.sleep(forTimeInterval: 10)
        }

        NSLog(AppDelegate.TAG + "Task complete!")
        task.setTaskCompleted(success: true)
    }

My expectation is that iOS will execute the launch handler 1 min after scheduling the background task. But that never happens, even after hours. Task did get executed within 20 mins (Sample output shown below), but every other time, I waited for more than an hour and task was never executed.

// Output
...
Task submitted successfully!
Background task scheduled successfully!!
Scheduled date = Optional(2023-02-06 09:19:46 +0000) | Present date = 2023-02-06 09:18:46 +0000
TaskRefreshCount1Min(task:)
1) 2023-02-06 09:35:12 +0000
2) 2023-02-06 09:35:22 +0000
3) 2023-02-06 09:35:32 +0000

The documentation does state,

However, the system doesn’t guarantee launching the task at the specified date, but only that it won’t begin sooner.

But I didn't expect the results to differ by such a huge margin.

Apparently others have also faced this 'inconsistent scheduling' in iOS.

  1. Background fetch with BGTaskScheduler works perfectly with debug simulations but never works in practice
  2. How make iOS app running on background Every one minute in Swift?

In links I have pasted above, despite Background Fetch being the most suitable for periodic light tasks, Silent Push Notifications were suggested. I'm currently checking out Silent Push Notifications, but iOS recommends a frequency of not more than 3 per hour.

Summarising my questions from this exercise:

  1. Is there something wrong with the code?
  2. What technique should I use? Background fetch, silent notifications or anything else?
  3. In Background Fetch, how do you schedule background tasks periodically? There's an instance property called earliestBeginDate to denote the minimum time after which the task can happen.
Post not yet marked as solved Up vote post of GangOrca Down vote post of GangOrca
1.7k views

Replies

Update1: Correcting a typo

New: Example: The sample background task is to count 60s in multiple of 10s.

Old: Example: The sample background task is to count 10s in multiple of 10s.

Update 2: Missed to add some details and another question :(

  1. Expiration handler doesn't get called (unlike the case with beginBackgroundTask(name:expirationHandler:))?

Environment: Xcode 14.2, iOS 16.0, Debug and Release build, Console app (to check logs).