macOS launch sequence¶
@main
final class AppDelegate: NSObject, NSApplicationDelegate {
// 1. Construct.
let backgrounder = Backgrounder.companion.create()
func applicationDidFinishLaunching(_ notification: Notification) {
// 2. Register every worker factory.
backgrounder.register(taskId: SyncWorker.companion.ID) {
SyncWorker(repo: AppGraph.shared.repository)
}
// 3. Start — sweeps ephemeral state and seals the registry.
backgrounder.start()
}
func applicationWillTerminate(_ notification: Notification) {
// Cancel the scheduler's coroutine scope cleanly.
backgrounder.shutdown()
}
}
NSBackgroundActivityScheduler owns scheduling lifetime entirely, so unlike iOS there's no per-cold-launch handler-registration ceremony. start() does just two things on macOS:
- Sweep ephemeral state.
- Seal the
WorkerRegistryso furtherregister()calls throw.
What runs where¶
NSBackgroundActivityScheduler.scheduleWithBlockinvokes a closure on a system queue.- The library bounces into a
SupervisorJob+Dispatchers.Defaultscope (the same pattern as iOS), runs the worker, mapsWorkResulttoNSBackgroundActivityResultFinished(Success / Failure) orNSBackgroundActivityResultDeferred(Retry). cancel(taskId)callsinvalidate()on the live scheduler — interrupts the running block.cancelsInFlight = true.
Network constraints — library-managed gate¶
NSBackgroundActivityScheduler has no constraint concept of its own — no requiresNetworkConnectivity, no requiresExternalPower. Without the library filling the gap, WorkConstraints.networkRequired would be silently ignored on macOS.
The library inserts a pre-execution reachability gate that waits up to min(5 s, ctx.capabilities.maxExecutionTime / 4) (collapses to ≈5 s under the conservative 5-minute macOS budget) for the requirement to become true. On timeout the worker is short-circuited to WorkResult.Retry; handleOneShotRetry reschedules a fresh activity with interval = backoff.delayFor(attempt). See Recipes → Require a network connection.
Unmetered is honoured against ReachabilityStatus.isDataMetered == false. Power constraints (requiresCharging) are still not implemented on macOS — workers that need charging should check inside execute() and return Retry.
Periodic is native¶
macOS doesn't need the iOS periodic-emulation state machine. NSBackgroundActivityScheduler has repeats = true, interval, and tolerance (mapped from WorkRequest.Periodic.flexWindow). The library hands the OS a single repeating activity per task id and lets it dispatch.
Force-quit isn't a problem¶
Unlike iOS, force-quitting a macOS app doesn't disable background scheduling for the next launch. The active schedulers re-establish themselves when the user re-launches the app and backgrounder.start() runs again.
Shutdown¶
backgrounder.shutdown() cancels the scheduler's SupervisorJob-rooted scope. Call from applicationWillTerminate to tear down cleanly. Without it, in-flight workers continue until the OS reclaims the process — for a foreground app being explicitly quit, that's a few extra seconds of work that never matters; for a long-lived agent it can leave file handles open. Always pair with applicationWillTerminate.