Quickstart
This assumes Installation is done:
the package resolves, the four Info.plist keys are in place, and the
location background mode is enabled.
The whole thing
import BackgroundGeolocationimport SwiftUI
@mainstruct MyApp: App { var body: some Scene { WindowGroup { ContentView() .task { // Subscribe BEFORE ready(): events buffered during startup // are replayed to the first subscriber, so nothing that // happens while the engine comes up is lost. Task { for await location in BackgroundGeolocation.locations { print(location.coords.latitude, location.coords.longitude) } }
do { try await BackgroundGeolocation.ready( Config(distanceFilter: 10, stopTimeout: 5, debug: true) ) _ = try await BackgroundGeolocation.requestPermission() try await BackgroundGeolocation.start() } catch { print("bgeo:", error) } } } }}That is a complete tracker. What each part is doing:
ready(config) — configure, do not start
ready() applies configuration and brings the engine up; it does not begin
tracking. It returns the State
snapshot, whose enabled tells you whether tracking was already running from a
previous process — the normal case after a background relaunch:
let state = try await BackgroundGeolocation.ready(config)if state.enabled { // Already tracking; do not call start() again, and reflect it in the UI.}Call it on every launch with the same config. It is idempotent, and it is what re-attaches your process to tracking that never stopped.
requestPermission() — before start()
Covered in Permissions. Skipping it
does not fail loudly: start() succeeds and the engine runs, it just never
receives a fix.
start() — begin tracking
try await BackgroundGeolocation.start() // persists; survives relaunchtry await BackgroundGeolocation.stop() // the only thing that turns it offstart() persists the intent. Tracking continues across backgrounding,
force-quit and reboot until something calls stop().
Consuming events
AsyncStream for structured concurrency:
Task { for await event in BackgroundGeolocation.motionChanges { print("isMoving:", event.isMoving) }}Or a callback where a stream is awkward:
let subscription = BackgroundGeolocation.onLocation { location in /* ... */ }subscription.remove()Every event has both forms — see Events. Each
access to a stream property mints a new subscription, because AsyncStream is
single-consumer: two for await loops over locations each need their own.
A one-shot position
let location = try await BackgroundGeolocation.getCurrentPosition( CurrentPositionOptions(samples: 1, timeout: 30))Independent of start(): it works while tracking is off and does not turn it
on.
Sending locations somewhere
try await BackgroundGeolocation.ready( Config( distanceFilter: 10, url: "https://api.example.com/locations", autoSync: true, batchSync: true, maxBatchSize: 50, authorization: AuthorizationConfig( strategy: "JWT", accessToken: accessToken, refreshToken: refreshToken, refreshUrl: "https://api.example.com/auth/refresh" ) ))Uploads run inside the engine with a durable queue behind them — no Swift of yours in the hot path, and nothing lost when the process dies. See HTTP & authorization.
Seeing what the engine is doing
try await BackgroundGeolocation.ready(Config(debug: true, logLevel: LogLevel.debug.rawValue))debug plays a sound on each significant event; logLevel persists the
engine’s own diagnostics, readable with getLog(). Both are development aids.
Logging & debugging covers the rest.
The same lines reach Xcode’s console through os_log; the
example app’s Logs tab (above) shows them
on the device itself, interleaved with the native engine’s own rows.
Next
- Tracking lifecycle — when the engine decides you are moving
- Config — every key
- Example app — a full console with a map, logs and settings