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 and idle constraints (requiresCharging, requiresDeviceIdle) are not enforced on macOS — NSBackgroundActivityScheduler exposes neither, though it already biases toward idle moments via qualityOfService = .background. Workers needing a charging or idle precondition 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 on macOS¶
survivesForceQuit = false — NSBackgroundActivityScheduler is in-process; all registered activities die with the process, and the library does not persist schedules across launches on macOS. Unlike iOS, macOS does not blacklist the app from future background dispatch: anything you schedule after relaunch dispatches normally. Re-schedule from your app's init path after backgrounder.start() at each launch.
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.