Skip to content

Boot & killed-app behaviour

BGeo’s contract is that tracking and upload happen inside the engine, and your code is optional at runtime. Once start() has run, the engine keeps recording, filtering, persisting to SQLite and uploading whether or not a single line of your Swift is executing.

iOS is the harder of the two platforms here, because it has no equivalent of a foreground service: an app that is not in front of the user is a candidate for suspension and termination at all times. What follows is what the engine actually does about that, and what was measured on a device.

The matrix

ScenarioWhat happensGap
BackgroundedThe session stream stays alive and keeps delivering. No relaunch needed.None
Suspended while parkedliveUpdates auto-pauses as the parked keep-alive; the app suspends. Movement auto-resumes delivery in the background, with no foreground visit.None once movement resumes
OS-killed (memory eviction)iOS relaunches the process for a location event; the engine recreates its session synchronously in init, continuing the authorization grant it held.~1 s
Force-quit (swiped from the app switcher)Same path. Device-verified across three consecutive force-quit deaths: relaunch in ~1 second each time.~1 s
Device rebootNo boot broadcast exists on iOS, so startOnBoot is best-effort by design: the wake region and significant-location-change registrations survive in locationd and relaunch the app. Device-verified at ~1 minute after boot, parked, with no movement required.~1 min

Why this works — and what it costs

The mechanism is the session engine (see Tracking lifecycle):

  1. CLLocationUpdate.liveUpdates delivers while moving and auto-pauses while parked instead of being stopped. Its documented auto-resume is what revives background delivery without a foreground visit.
  2. CLBackgroundActivitySession keeps the app eligible for background location runtime — and shows the blue indicator for as long as it lives. That indicator is the price of the whole arrangement; there is no API to hide it on this path.
  3. CLServiceSession (iOS 18+) declares a standing authorization need, which preserves the grant across suspension and termination.
  4. On relaunch, the session is recreated synchronously in the engine’s init — promptly enough that iOS treats it as a continuation of the grant the previous process held, rather than a fresh background request (which iOS ignores until the next foreground).

stopOnTerminate

stopOnTerminate (default false) decides whether tracking should stop when the app is killed.

iOS gives no reliable terminate hook, so the flag is honoured at the next relaunch: the engine drops the persisted enabled flag in init, before any auto-resume path runs. Your app sees state.enabled == false on the next ready() and has to call start() again.

There is therefore a window between the kill and the next relaunch where nothing has “stopped” in any observable sense — the process is simply gone — but no fixes are produced either way.

startOnBoot

startOnBoot (default false) decides whether tracking resumes after a reboot.

true is best-effort, as described in the matrix: locationd keeps the wake region and SLC registrations across the reboot and relaunches the app.

false is honoured exactly: the engine detects the reboot via kern.boottime (persisted, with a 60-second clock-drift tolerance — deliberately not systemUptime, which pauses while the device sleeps) and drops the persisted enabled flag before any auto-resume runs. Later relaunches find tracking disabled and do nothing.

Verifying it yourself

The Simulator cannot show you any of this. On a device:

Force-quit: start tracking, swipe the app away, then walk or drive. Watch the server, or reconnect and read getLog() — you are looking for a session.relaunch line seconds after the kill.

Overnight eviction: leave the app backgrounded overnight, then drive in the morning without opening it. This is the case that matters most in practice and the one that used to fail: the fix stream simply continuing is the result.

Reboot: enable startOnBoot, start tracking, reboot, and leave the phone parked. Tracking should come back on its own in about a minute.

After any of these, getState() tells you what the engine thinks: lastAcceptedFixAge (seconds, nil until the first fix) and sessionEngineActive answer “did it really come back” without guessing. See Logging & debugging.