← All shorts
73iOS 13+2 min read

BGTaskScheduler: Work That Survives Backgrounding

BGTaskScheduler lets iOS decide when to wake your app for maintenance work instead of you fighting for background time.

Legacy background fetch gave the app a single opaque callback and roughly 30 seconds whenever the system felt like invoking it, with no way to express constraints like "only when charging" or "only for longer maintenance work."

#The old way

import UIKit

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        SyncService.shared.sync { success in
            completionHandler(success ? .newData : .failed)
        }
    }
}

#The new way

import BackgroundTasks

let refreshTaskIdentifier = "com.example.app.refresh"

func registerBackgroundTasks() {
    BGTaskScheduler.shared.register(forTaskWithIdentifier: refreshTaskIdentifier, using: nil) { task in
        handleRefresh(task: task as! BGAppRefreshTask)
    }
}

func scheduleRefresh() {
    let request = BGAppRefreshTaskRequest(identifier: refreshTaskIdentifier)
    request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 60)
    try? BGTaskScheduler.shared.submit(request)
}

func handleRefresh(task: BGAppRefreshTask) {
    scheduleRefresh()

    let operation = Task {
        await SyncService.shared.sync()
        task.setTaskCompleted(success: true)
    }

    task.expirationHandler = {
        operation.cancel()
    }
}

#Why it matters

  • BGTaskScheduler covers both short refresh tasks (BGAppRefreshTaskRequest) and longer processing tasks (BGProcessingTaskRequest, for things like database maintenance) with the same registration model
  • Scheduling is declarative — submit a request with constraints (earliest date, requires network, requires charging for processing tasks) and the system decides the actual moment
  • expirationHandler gives a clean signal to cancel in-flight work before the system kills the process outright
  • Replaces scattered beginBackgroundTask/endBackgroundTask pairs with one scheduling API for planned, recurring work

#Gotcha

earliestBeginDate is a floor, not a promise — the system weighs battery level, Background App Refresh settings, and how often the user actually opens the app, and a task can run much later than requested or not at all in a given session. Always reschedule the next request from inside the handler, as above, rather than assuming one submission recurs automatically. Testing this reliably requires triggering it manually through the debugger (Apple documents an e -l objc -- LLDB command for simulating a launch) — it won't fire naturally on a timeline short enough to test by just waiting.

background-tasksiosbgtaskscheduler

Related shorts