Background Tasks runs foreground

Hello everyone!

I'm having a problem with background tasks running in the foreground.

When a user enters the app, a background task is triggered. I've written some code to check if the app is in the foreground and to prevent the task from running, but it doesn't always work. Sometimes the task runs in the background as expected, but other times it runs in the foreground, as I mentioned earlier.

Could it be that I'm doing something wrong? Any suggestions would be appreciated.

here is code:

class BackgroundTaskService {
    @Environment(\.scenePhase) var scenePhase
    static let shared = BackgroundTaskService()
    
    private init() {}
    
    // MARK: - create task
    func createCheckTask() {
        let identifier = TaskIdentifier.check
        BGTaskScheduler.shared.getPendingTaskRequests { requests in
            if requests.contains(where: { $0.identifier == identifier.rawValue }) {
                return
            }
            self.createByInterval(identifier: identifier.rawValue, interval: identifier.interval)
        }
    }
    
    private func createByInterval(identifier: String, interval: TimeInterval) {
        let request = BGProcessingTaskRequest(identifier: identifier)
        request.earliestBeginDate = Date(timeIntervalSinceNow: interval)
        scheduleTask(request: request)
    }
    
    // MARK: submit task
    private func scheduleTask(request: BGProcessingTaskRequest) {
        do {
            try BGTaskScheduler.shared.submit(request)
        } catch {
           // some actions with error
        }
    }
    
    // MARK: background actions
    func checkTask(task: BGProcessingTask) {
        let today = Calendar.current.startOfDay(for: Date())
        let lastExecutionDate = UserDefaults.standard.object(forKey: "lastCheckExecutionDate") as? Date ?? Date.distantPast
        let notRunnedToday = !Calendar.current.isDate(today, inSameDayAs: lastExecutionDate)
        guard notRunnedToday else {
            task.setTaskCompleted(success: true)
            createCheckTask()
            return
        }
        if scenePhase == .background {
            TaskActionStore.shared.getAction(for: task.identifier)?()
        }
        task.setTaskCompleted(success: true)
        UserDefaults.standard.set(today, forKey: "lastCheckExecutionDate")
        createCheckTask()
    }
}

And in AppDelegate:

BGTaskScheduler.shared.register(forTaskWithIdentifier: "check", using: nil) { task in
	guard let task = task as? BGProcessingTask else { return }
        BackgroundTaskService.shared.checkNodeTask(task: task)
}
BackgroundTaskService.shared.createCheckTask()
Answered by DTS Engineer in 814274022

When a user enters the app, a background task is triggered. I've written some code to check if the app is in the foreground and to prevent the task from running, but it doesn't always work. Sometimes the task runs in the background as expected, but other times it runs in the foreground, as I mentioned earlier.

First off, what are you actually testing this on and where are you getting the reports from? In particular, are you seeing "real world" issue with this form end user devices and/or your "own" phone/devices (meaning, a device that you use "normally")? Or are you seeing this on dedicated development devices?

The issue with dedicated development devices is that background tasks scheduling is HEAVILY driven app usage patterns and by the overall state of the device, both of which are very weirdly distorted on dedicated development devices. That's because:

  1. The device tends to be plugged in "all the time", so the battery is always full and the system expects that it will always be full.

  2. The device tends to spend "all" of it's time running a very small number of apps, typically "your app(s)" and maybe a few other test apps.

  3. The device is used for very little "else", so there's very little "competition" for scheduling resources.

Those factors together can make task scheduling go a bit bonkers. The system ends up both wildly over-prioritizing your app (because of #2) and while also having far more "eligible" time (due to #1 and #3) for task execution than it normally would. None of this is really a "problem", but you do need to be aware that you can see things happening that would NEVER happen unde real world conditions.

In terms of "real world usage", my general expectation is that BGProcessingTask's will be processed "every day or so" while the device is charge "overnight". In practical terms, most users plug their device in before they go to sleep and the scheduling system's "goal" is to run processing task in the "middle" of that time window, since that's the time the user is least likely to need/use their phone in any way.

Moving to your code, I do have a few points:

  • As a general note, I strongly recommend taking a close look at the "Refreshing and Maintaining Your App Using Background Tasks" sample project, paying attention to the details of how it handles task. For example, it leaves BGProcessingTask as "pending" (instead of aggressively rescheduling them) but cancels and reschedules them if they get "old enough". Leaving them pending helps ensure they run (older tasks have higher scheduling priority) but "old enough" tasks should be rescheduled.

  • I don't think your code does this, but keep in mind that the SINGLE most common mistake with this API is aggressively rescheduling task at launch. What ends up happening is that system launches your app to run your task and your app then reschedules the task for "the future", so the systems suspends your app again... because you don't have any tasks to run.

  • Your app should not setTaskCompleted until it as finished ALL the work it wants to complete. Notably, if your "TaskActionStore.shared.getAction" code is triggering asynchronous work, then I'd fully expect that work to complete at some "later" point, NOT at BGProcessingTask time. Your app would have suspended shortly after returning from "checkTask", likely before any work could have started. It would then start the next time your app was woken.

...
        if scenePhase == .background {
            TaskActionStore.shared.getAction(for: task.identifier)?()
        }
        task.setTaskCompleted(success: true)
...
  • I'm not sure what you're scheduling strategy is but most apps should be scheduling relatively few tasks and that scheduling should be based on the expected interval, NOT "work". In other words, an app with 10 daily tasks and 5 weekly tasks should have 2 task ("daily" and "weekly"), not 15.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

When a user enters the app, a background task is triggered. I've written some code to check if the app is in the foreground and to prevent the task from running, but it doesn't always work. Sometimes the task runs in the background as expected, but other times it runs in the foreground, as I mentioned earlier.

First off, what are you actually testing this on and where are you getting the reports from? In particular, are you seeing "real world" issue with this form end user devices and/or your "own" phone/devices (meaning, a device that you use "normally")? Or are you seeing this on dedicated development devices?

The issue with dedicated development devices is that background tasks scheduling is HEAVILY driven app usage patterns and by the overall state of the device, both of which are very weirdly distorted on dedicated development devices. That's because:

  1. The device tends to be plugged in "all the time", so the battery is always full and the system expects that it will always be full.

  2. The device tends to spend "all" of it's time running a very small number of apps, typically "your app(s)" and maybe a few other test apps.

  3. The device is used for very little "else", so there's very little "competition" for scheduling resources.

Those factors together can make task scheduling go a bit bonkers. The system ends up both wildly over-prioritizing your app (because of #2) and while also having far more "eligible" time (due to #1 and #3) for task execution than it normally would. None of this is really a "problem", but you do need to be aware that you can see things happening that would NEVER happen unde real world conditions.

In terms of "real world usage", my general expectation is that BGProcessingTask's will be processed "every day or so" while the device is charge "overnight". In practical terms, most users plug their device in before they go to sleep and the scheduling system's "goal" is to run processing task in the "middle" of that time window, since that's the time the user is least likely to need/use their phone in any way.

Moving to your code, I do have a few points:

  • As a general note, I strongly recommend taking a close look at the "Refreshing and Maintaining Your App Using Background Tasks" sample project, paying attention to the details of how it handles task. For example, it leaves BGProcessingTask as "pending" (instead of aggressively rescheduling them) but cancels and reschedules them if they get "old enough". Leaving them pending helps ensure they run (older tasks have higher scheduling priority) but "old enough" tasks should be rescheduled.

  • I don't think your code does this, but keep in mind that the SINGLE most common mistake with this API is aggressively rescheduling task at launch. What ends up happening is that system launches your app to run your task and your app then reschedules the task for "the future", so the systems suspends your app again... because you don't have any tasks to run.

  • Your app should not setTaskCompleted until it as finished ALL the work it wants to complete. Notably, if your "TaskActionStore.shared.getAction" code is triggering asynchronous work, then I'd fully expect that work to complete at some "later" point, NOT at BGProcessingTask time. Your app would have suspended shortly after returning from "checkTask", likely before any work could have started. It would then start the next time your app was woken.

...
        if scenePhase == .background {
            TaskActionStore.shared.getAction(for: task.identifier)?()
        }
        task.setTaskCompleted(success: true)
...
  • I'm not sure what you're scheduling strategy is but most apps should be scheduling relatively few tasks and that scheduling should be based on the expected interval, NOT "work". In other words, an app with 10 daily tasks and 5 weekly tasks should have 2 task ("daily" and "weekly"), not 15.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Background Tasks runs foreground
 
 
Q