Skip to content

Events

Each event has two equivalent shapes: an AsyncStream property, and an onX(_:) callback returning a Subscription. Same events, same payloads.

Stream or callback?

A callback runs inline, on the main actor, when the engine emits. It is never dropped, and a slow handler holds up the emitting path until it returns.

An AsyncStream buffers and hands you events in a for await loop, so a slow consumer does not block the engine.

AsyncStream is single-consumer. Each access to a stream property mints a fresh subscription, so two for await loops over locations each get their own stream and both see every fix — but iterating the same stream value from two places splits the events between them, which is almost never what you want. Hold one loop per stream, or use callbacks.

Subscribe before ready()

Events that arrive while the engine is coming up are buffered and replayed to the first subscriber, so subscribing before ready() loses nothing. Subscribing after it can miss the launch-time providerchange a cold start emits.

onLocation

for await location in BackgroundGeolocation.locations { }
let subscription = BackgroundGeolocation.onLocation { location in }

Every accepted fix, after filtering. Payload: Location.

Fixes rejected by the accuracy or teleport filter never appear here — if the stream is quiet, compare lastRawFixAge and lastAcceptedFixAge in getState() before assuming the OS stopped delivering.

onMotionChange

for await event in BackgroundGeolocation.motionChanges { }
let subscription = BackgroundGeolocation.onMotionChange { event in }

The motion state machine flipped between moving and stationary. Payload: MotionChangeEvent.

event.location is null on the first motion change of a session — the engine has not accepted a fix yet at that point.

onProviderChange

for await state in BackgroundGeolocation.providerChanges { }
let subscription = BackgroundGeolocation.onProviderChange { state in }

Location authorization or provider availability changed: the user granted or revoked permission, turned location services off, or switched to approximate location. Payload: ProviderState.

This is how you learn tracking has gone quiet for a reason you can explain to the user, rather than discovering it from an empty map.

onHeartbeat

for await event in BackgroundGeolocation.heartbeats { }
let subscription = BackgroundGeolocation.onHeartbeat { event in }

Fires every heartbeatInterval seconds while stationary. It is a keep-alive tick, not a location: use it to poll getCurrentPosition() if you need a fix while parked, and expect Doze to stretch the interval.

onHttp

for await event in BackgroundGeolocation.httpEvents { }
let subscription = BackgroundGeolocation.onHttp { event in }

The result of every upload request the engine made. Payload: HttpEvent.

status = 0 means the request never reached a server (DNS, TLS, no route) — distinct from a 5xx, and the engine retries it rather than dropping the record.

onConnectivityChange

for await event in BackgroundGeolocation.connectivityChanges { }
let subscription = BackgroundGeolocation.onConnectivityChange { event in }

Validated internet connectivity changed — a captive portal counts as disconnected, unlike a bare “interface up” signal.

onPowerSaveChange

for await isPowerSaveMode in BackgroundGeolocation.powerSaveChanges { }
let subscription = BackgroundGeolocation.onPowerSaveChange { isPowerSaveMode in }

The device entered or left Low Power Mode. The engine unwraps the native {isPowerSaveMode} payload to a bare Bool.

Low Power Mode throttles background location hard; this event is often the explanation for a track that suddenly goes sparse.

onAuthorization

for await json in BackgroundGeolocation.authorizationEvents { }
let subscription = BackgroundGeolocation.onAuthorization { json in }

The engine’s HTTP layer refreshed (or failed to refresh) your JWT pair. Payload is a raw [String: Any]: {success, accessToken, refreshToken}.

It carries live credentials. Persist them if your app keeps its own copy — see getAuthState() — but do not log the object. The BGeo example console logs only presence booleans for exactly this reason.

onLocationError

for await error in BackgroundGeolocation.locationErrors { }
let subscription = BackgroundGeolocation.onLocationError { error in }

A watchPosition() tick failed, or watchPosition() was called on an unlicensed build. Payload: LocationErrorEvent.

Both of those sites emit this event instead of throwing, so a failing watch fails in complete silence without a subscriber here. If you use watchPosition(), subscribe to this.

onGeofence

for await event in BackgroundGeolocation.geofenceEvents { }
let subscription = BackgroundGeolocation.onGeofence { event in }

A geofence transition: ENTER, EXIT or DWELL. Payload: GeofenceEvent.

onGeofencesChange

for await event in BackgroundGeolocation.geofenceChanges { }
let subscription = BackgroundGeolocation.onGeofencesChange { event in }

The set of geofences the OS is actively monitoring changed as you moved — Android caps how many regions one app can watch, so the engine keeps the nearest ones armed. Payload: GeofencesChangeEvent.

Both geofence events live in the same extension as the geofence CRUD, so they come with the one import BackgroundGeolocation.

Unsubscribing

let subscription = BackgroundGeolocation.onLocation { _ in }
subscription.remove()

A stream stops when its for await loop ends — cancel the Task holding it. Dropping the Subscription value without calling remove() leaves the callback registered, so keep it if you ever need to detach.