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
| Scenario | What happens | Gap |
|---|---|---|
| Backgrounded | The session stream stays alive and keeps delivering. No relaunch needed. | None |
| Suspended while parked | liveUpdates 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 reboot | No 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):
CLLocationUpdate.liveUpdatesdelivers while moving and auto-pauses while parked instead of being stopped. Its documented auto-resume is what revives background delivery without a foreground visit.CLBackgroundActivitySessionkeeps 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.CLServiceSession(iOS 18+) declares a standing authorization need, which preserves the grant across suspension and termination.- 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.