Data types
Every type is a Swift struct decoded from the engine’s dictionaries. Fields
the engine may legitimately omit are optional; fields it always sends are not.
Where a field is optional, that optionality is load-bearing — see the note at
the end.
Location
The payload of onLocation, the return of
getCurrentPosition(), and the record shape in the upload queue.
| Field | Type | Notes |
|---|---|---|
uuid | String | Stable per record; the handle destroyLocation() takes. |
timestamp | String | ISO 8601, UTC. |
age | Double? | Milliseconds between the fix being taken and processed. |
odometer | Double | Cumulative metres since the last reset. |
coords | Coords | See below. |
activity | MotionActivity | Classification at the time of the fix. |
battery | Battery | Level and charging state. |
isMoving | Bool | The motion machine’s verdict for this fix. |
sample | Bool? | true for a getCurrentPosition() sample that was not persisted. |
event | String? | "motionchange", "heartbeat", "geofence", or absent for an ordinary fix. |
extras | [String: Any]? | Whatever you attached via config or getCurrentPosition(extras = …). |
Coords
| Field | Type | Notes |
|---|---|---|
latitude | Double | |
longitude | Double | |
accuracy | Double | Horizontal radius, metres — the number the accuracy gate filters on. |
altitude | Double? | Metres above sea level. |
altitudeAccuracy | Double? | |
speed | Double? | m/s. -1 means “unknown” on some devices; treat negatives as absent. |
speedAccuracy | Double? | |
heading | Double? | Degrees; negative means unknown. |
headingAccuracy | Double? | |
ellipsoidalAltitude | Double? | Height above the WGS84 ellipsoid, where the device reports it. |
MotionActivity
| Field | Type | Notes |
|---|---|---|
type | ActivityType | STILL, WALKING, IN_VEHICLE, … |
confidence | Int | 0–100, from Play Services activity recognition. |
The engine ignores a classification below minimumActivityRecognitionConfidence
(75 by default) when deciding motion state, so a low-confidence IN_VEHICLE
does not by itself wake tracking.
Battery
| Field | Type | Notes |
|---|---|---|
level | Double | 0.0–1.0. -1 means unknown — guard before showing a percentage. |
isCharging | Bool |
State
Returned by ready(), setConfig(), start(), stop() and getState().
| Field | Type | Notes |
|---|---|---|
enabled | Bool | Whether tracking is on. This is the persisted intent, not “is a fix arriving right now”. |
raw | [String: Any] | Everything else the engine reported. |
raw is deliberately untyped: it is a superset of health and diagnostic fields
that grows with the engine, and a typed class would have to be released in
lockstep to stay honest. Read it with state["key"], which returns null for
both an absent key and a JSON null:
let state = await BackgroundGeolocation.getState()let trackingActive = state["trackingActive"] as? Boollet lastFixAge = state["lastAcceptedFixAge"] as? Double // seconds, nil until a fix arrivesFields the iOS engine reports today: enabled, trackingMode, isMoving,
odometer, trackingActive, authorization, lastRawFixAge,
lastAcceptedFixAge, lastLocationError, locationFailureCount,
backgroundRearmCount, watchdogRecoveryCount, wakeRearmCount,
stationaryRegionArmed, monitoredWakeRegions, lastWakeError,
rawFixCount, acceptedFixCount, rejectedFixCount, lastRejectReason,
sessionEngineActive, serviceSessionActive, geofenceCount,
lastGeofenceError, connected, headingAvailable.
The two fix ages are in seconds and nil until the first fix — the raw one
is stamped before the location filter runs and the accepted one after, so the
pair separates “CoreLocation stopped delivering” from “the filter is rejecting
everything”. That distinction is the first fork of every field investigation,
and rawFixCount/acceptedFixCount/rejectedFixCount/lastRejectReason
answer the follow-up.
sessionEngineActive tells you which delivery path is live (see
useSessionEngine), and
serviceSessionActive whether the iOS 18+ CLServiceSession is held.
headingAvailable (Bool) is the only gate on
watchHeading(_:) — it needs no
permission, just a magnetometer. Check it before promising a compass in your
UI; Android has no equivalent probe.
ProviderState
From getProviderState(), and the payload of onProviderChange.
| Field | Type | Notes |
|---|---|---|
status | AuthorizationStatus | The grant. |
enabled | Bool | Location services on at all. |
gps | Bool | GPS provider enabled. |
network | Bool | Network provider enabled. |
accuracyAuthorization | AccuracyAuthorization? | REDUCED when the user granted approximate location only. |
Geofence
| Field | Type | Notes |
|---|---|---|
identifier | String | Yours; unique. Re-adding the same identifier replaces the fence. |
radius | Double | Metres. Under ~100 m, expect the OS to report crossings late. |
latitude / longitude | Double | |
notifyOnEntry | Bool? | Defaults to true engine-side. |
notifyOnExit | Bool? | Defaults to true engine-side. |
notifyOnDwell | Bool? | Requires loiteringDelay. |
loiteringDelay | Double? | Milliseconds inside the radius before DWELL fires. |
extras | [String: Any]? | Echoed back on every event for this fence. |
Event payloads
MotionChangeEvent
| Field | Type | Notes |
|---|---|---|
isMoving | Bool | |
location | Location? | Nil on the first motionchange of a tracking session — the engine has no accepted fix yet. |
GeofenceEvent
| Field | Type | Notes |
|---|---|---|
identifier | String | |
action | GeofenceAction | .enter, .exit, .dwell. |
location | Location | The last accepted fix when the OS delivered the transition — not the point where the boundary was crossed. |
extras | [String: Any]? | From the geofence definition. |
GeofencesChangeEvent
| Field | Type | Notes |
|---|---|---|
on | [Geofence] | Now being monitored by the OS. |
off | [Geofence] | No longer monitored. |
iOS allows 20 monitored regions per app and the engine spends one on its own wake region, so 19 of yours are armed at a time. It keeps the nearest ones registered and swaps as you move. This event is how you see that happen; the fences you added all still exist.
HttpEvent
| Field | Type | Notes |
|---|---|---|
success | Bool | 2xx. |
status | Int | HTTP status; 0 means a network-level failure, not a server response. |
responseText | String | Body, truncated to 1024 chars. Logged verbatim — do not put secrets in error bodies. |
ConnectivityChangeEvent
| Field | Type |
|---|---|
connected | Bool |
Validated connectivity, not merely “an interface is up” — a captive portal reads as disconnected.
LocationErrorEvent
| Field | Type | Notes |
|---|---|---|
code | String | e.g. a CoreLocation error code. |
message | String? |
The payload of onLocationError.
code arrives as either a string or a number depending on which engine call
site emitted it, and is normalised to a string here — so a caller can switch on
it without knowing which path produced the failure.
HeartbeatEvent
| Field | Type |
|---|---|
raw | [String: Any] |
CrashEvent
| Field | Type | Notes |
|---|---|---|
timestamp | String | ISO-8601 UTC, the confirmation moment. |
peakG | Double | Peak impact magnitude, in g. |
impactDurationMs | Double | Duration of the impact signature, in ms. |
preImpactSpeedMps | Double | Speed immediately before impact, in m/s. |
location | Location? | Last known fix at confirmation time, or nil if none was available. |
The payload of onCrash. Armed only
while moving, off by default — see
Config.crashDetection.
DistractionEvent
| Field | Type | Notes |
|---|---|---|
timestamp | String | ISO-8601, the moment the episode closed. |
startTimestamp | String | ISO-8601, the episode start (same instant as the dds extras key below). |
durationSec | Double | Final episode duration in seconds (same value as the ddd extras key below). |
cause | String | "h" handling-only (iOS), "s" handling + active screen (Android), "c" reserved for calls (not implemented). |
location | Location? | Last known fix at close time, or nil if none was available. |
The payload of
onDistraction. Armed only
while moving, off by default — see
Config.distractionDetection.
This detects the phone being physically handled at driving speed — see
Limitations for what it cannot see.
Distraction extras keys
While an episode is open (and never lost to a sparse fix stream), the engine
stamps these keys into the extras of every uploaded Location — this is
the wire contract a backend parses, independent of the distraction event
above:
| Key | Type | When present | Meaning |
|---|---|---|---|
dds | number | On every uploaded fix while an episode is open, and on the closing fix. | Unix seconds of the episode start. This value doubles as the episode id. |
ddc | string | Alongside dds. | Cause: "h" handling-only (iOS), "s" handling + active screen (Android), "c" reserved for calls (not implemented). |
ddd | number | Only on the first fix persisted after the episode ends. | Final episode duration in seconds. |
Rules for a backend consuming these keys:
- Group fixes into an episode by equal
ddsvalue, not by adjacency in the fix stream — grouping by equal value survives gaps in delivery. - Episode duration is
dddfrom the closing fix. If the closing fix never arrives, fall back to the span of fixes carrying thatdds— this is only a lower bound. - A short episode can produce no fixes “inside” it. If an episode is
shorter than the interval between fixes, the next persisted fix simply
carries
dds,ddcanddddtogether. Episodes are therefore never lost to a sparse fix stream. - A pending close wins a race with a new open. If a new episode opens
before the previous episode’s close has been flushed to a fix, that fix
carries the pending close’s
dds/ddc/dddtriple in full; the newly opened episode’sdds/ddcpair is deferred to the next record. Grouping by equaldds(above) is unaffected either way. - Do not count episodes off the location stream. The closing triple
(
dds/ddc/ddd) rides every record built between the close and the first record that is actually persisted, so asample: trueone-shot and thelocationembedded inside another event can each carry it more than once. The uploaded-record contract is unaffected, but a consumer must count episodes from thedistractionevent instead — it fires once per episode in normal operation, but a rare disarm race can publish a seconddistractionevent for the same episode (samedds/startTimestamp, differentdurationSec/ddd). A consumer that needs exact counts should dedupe bystartTimestamp(equivalentlydds).
HeadingEvent
| Field | Type | Notes |
|---|---|---|
heading | Double | Smoothed azimuth in degrees, [0, 360), clockwise from north. |
accuracy | Double | CLHeading.headingAccuracy: the estimated error in degrees. A negative value means the reading is invalid (needs calibration). Deliberately not the same quantity as the Android facade’s HeadingEvent.accuracy, an Int calibration level 0–3 — neither converts to the other. |
isTrue | Bool | true when heading is relative to true north — Core Location needs a location fix to derive the declination. false (magnetic) until it has one. |
The payload of onHeading — see
watchHeading(_:).
Option classes
CurrentPositionOptions
| Field | Type | Notes |
|---|---|---|
persist | Bool? | Whether the fix enters the upload queue. |
samples | Int? | How many fixes to collect before returning the best. |
timeout | Double? | Seconds. |
maximumAge | Double? | Milliseconds; accept a cached fix younger than this. |
desiredAccuracy | Int? | Overrides config for this call. |
extras | [String: Any]? | Attached to the returned record. |
WatchPositionOptions
| Field | Type |
|---|---|
interval | Double? |
desiredAccuracy | Int? |
persist | Bool? |
extras | [String: Any]? |
WatchHeadingOptions
Parameter of watchHeading(_:).
Every field optional; nil lets the engine’s own default apply.
| Field | Type | Notes |
|---|---|---|
smoothingTauMs | Double? | Time constant of the exponential azimuth smoother, ms — larger is steadier and laggier. Default 120. |
minIntervalMs | Double? | Floor on the interval between two emitted events, ms. Default 40. |
minDeltaDeg | Double? | Floor on the change in heading, degrees, needed to emit before minIntervalMs has elapsed. Default 0.5. |
getAuthState()‘s tuple
(accessToken: String?, refreshToken: String?) — a tuple rather than a named
type, because it exists only to answer one question.
The engine’s current token pair after a native refresh — read it with
getAuthState() when your app needs to stay in step with tokens the SDK
rotated on its own. See HTTP & authorization.
LogEntry
| Field | Type | Notes |
|---|---|---|
ts | String | ISO 8601. |
level | Int | 1=ERROR … 5=VERBOSE. |
src | String | "native" for engine lines. |
event | String | Dot-namespaced (track.start, wake.rearm) for engine diagnostics. |
message | String? | |
data | Any? | Parsed JSON, not a string. |
A note on optionality
Optional fields here are not defensive padding. MotionChangeEvent.location
really is absent on the first motion change of a session, and isMoving
arrives as null from the engine while motion is still being probed — up to
stopTimeout minutes after every start().
That last one has history: this SDK originally declared isMoving
non-optional, on the strength of a TypeScript interface that said boolean,
and dropped every location for the first minutes of every session — while
the server received them all, because the upload path never touches the
decoder. If a field is optional in this table, the engine can and does send
nothing for it.