Skip to content

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 BackgroundGeolocation
import SwiftUI
@main
struct 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 relaunch
try await BackgroundGeolocation.stop() // the only thing that turns it off

start() 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 iOS example app's Logs tab during a tracking run: onLocation rows with coordinates and accuracy, an onHeartbeat row, and a session.summary line reporting fixes and average accuracy, above the level filter chips.

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