iOS SDK Usage & Integration
In-SDK Geofencing is the recommended way to integrate: hand the SDK your stops and it starts and stops tracking automatically as the driver enters and exits each one. Prefer to drive geofence enter/exit yourself? See Manual Geofencing below.
In-SDK Geofencing
Hand the SDK your stops and it auto-starts and stops a tracking session as the driver enters and exits each one.
This flow has four touchpoints in your driver app (Initialize SDK is shared with the manual flow below):
| # | When this happens in the driver app | What you call |
|---|---|---|
| 1 | Driver app launches | Initialize SDK |
| 2 | Start geofencing and tracking all stops | Start Geofencing |
| 3 | Driver confirms drop-off or takes proof of delivery | Mark Drop-off |
| 4 | Stop geofencing and tracking all stops | Stop Geofencing |
If the driver's route changes mid-shift, call Update Stops as well.
Tracking needs location permission, and Always ("Change to Always Allow") is what locked-phone
and backgrounded tracking — plus route geofencing — actually require: with When-In-Use only, iOS
stops delivering fixes shortly after the screen locks. The full flow (the checkPermissions /
requestPermissions whitelist API, the eight states, and a staged prompt recipe) is on
iOS Permissions.
How It Works
- You pass the route as an array of
DeliveryStop; the SDK geofences the nearest stops for the route's lifetime. - On ENTER, it auto-starts a session for that stop (same as
startDeliveryByAddressString). - You call
markDropoffwhen the driver completes the stop. - On EXIT, the session stops only if
markDropoffwas called. Otherwise it runs untiltimeoutSeconds, so a false EXIT can't cut a delivery short. - Every auto-start/stop emits a
GeofenceSessionEventfor your UI.
The SDK handles the rest automatically: it absorbs GPS jitter (enter/exit hysteresis + debounce) and tightens location cadence near stops to balance precision and battery. These are tuned via remote config, not in your app.
API Reference
| Function | Description |
|---|---|
startRouteGeofencing(stops:options:) | Begins geofencing for stops ([DeliveryStop]), tuned by options (RouteGeofenceOptions). Throws if stops is empty or the SDK isn't initialized/eligible |
markDropoff(deliveryId:dropoffType:) | Marks a stop's drop-off, gating its geofence EXIT stop. Shared with the manual flow (see Mark Drop-off) |
updateRouteStops(stops:) | Replaces the active stop set ([DeliveryStop]) mid-route and re-diffs registration. A session for a removed stop is auto-stopped (tagged removed_from_route) |
stopRouteGeofencing() | Clears all geofences and stops active route sessions |
resumeRouteGeofencingIfNeeded() | Restores geofencing from persisted state. Call on every app launch; no-op if no route is active |
monitoredStops | Property. Returns the stops currently registered ([DeliveryStop], empty if no route is active) |
addGeofenceSessionListener(_:) | Subscribe to auto start/stop (GeofenceSessionEvent) events. Returns a subscription; call .remove() to unsubscribe |
1. Initialize SDK
Initialize the SDK once, early in your app's lifecycle, typically in your App struct or AppDelegate.
import SwiftUI
import DoorstepDropoffSDK
@main
struct MyApp: App {
init() {
// Initialize DoorstepAI with your API key.
// The SDK kicks off a config fetch immediately by default.
DoorstepAI.setApiKey(key: "YOUR_API_KEY_HERE")
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
setApiKey parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
key | String | n/a | JWT issued by DoorstepAI; sent as Authorization: Bearer <key> |
shouldGetConfig | Bool | true | When true, fetch config immediately. When false, defer until the first startDelivery* call |
Request permissions
Keep foreground and background location as separate, user-visible stages. Request the other collection buckets when your UI has explained their purpose:
@MainActor
func askForForegroundTrackingPermissions() {
DoorstepAI.requestPermissions([
.locationWhenInUse,
.motionFitness,
.bluetooth
])
}
@MainActor
func askForBackgroundTrackingAfterRationale() {
// Call from a later "Continue" action after When In Use is granted.
DoorstepAI.requestPermissions([.locationAlways])
}
Call each stage from a user-initiated onboarding or "Start shift" action—not from init(), app
launch, or mid-delivery. The calls are @MainActor; a SwiftUI button action already runs in the
right context. iOS controls when it offers the final Always upgrade, so re-check the detailed
.locationAlways state after each phase.
Need to ask for buckets individually, read the result, or check state without prompting? See
iOS Permissions — it covers the whitelist form, every state, and the
staged prompt flow. The old
DoorstepAI.requestAllPermissions(...) is the superseded compatibility requester and still works
unchanged. New integrations should use the typed whitelist API above.
Store your API key securely using environment variables or a secure configuration service. Never hardcode API keys in production builds.
2. Start Geofencing
Call startRouteGeofencing once you have the driver's stops for the shift. Pass every stop up front, not one at a time:
Task {
do {
try await DoorstepAI.startRouteGeofencing(
stops: myRouteStops, // [DeliveryStop]
options: myGeofenceOptions // RouteGeofenceOptions(), defaults if omitted
)
} catch {
// Handle empty stops or an ineligible SDK state
}
}
3. Mark Drop-off
When the driver completes the delivery (takes a POD or confirms drop-off in-app), mark it. markDropoff is the preferred API:
The call is identical for both flows. With In-SDK Geofencing, marking drop-off also gates the automatic geofence EXIT stop for that delivery: the session keeps running until it's called (or the timeout backstop fires).
Task {
do {
try await DoorstepAI.markDropoff(
deliveryId: "delivery_12345",
dropoffType: .pod // or .non_pod
)
} catch {
// Handle error
}
}
markDropoff parameters
| Parameter | Type | Description |
|---|---|---|
deliveryId | String | Required. The session you're marking |
dropoffType | DropoffType | .pod (proof of delivery captured) or .non_pod |
Custom events with newEvent
Beyond drop-offs, newEvent tags any custom event on the active session (for example, when the driver starts capturing a photo):
Task {
try await DoorstepAI.newEvent(
eventName: "taking_pod",
deliveryId: "delivery_12345",
timestamp: 1720000000 // optional epoch seconds; omit for "now"
)
}
| Parameter | Type | Description |
|---|---|---|
eventName | String | Required. Name of the event to record on the session |
deliveryId | String | Required. The active session to attach the event to |
timestamp | Double? | Epoch seconds for the event; omit it to use the current time |
4. Stop Geofencing
Call stopRouteGeofencing when the shift ends (or the driver logs out). It clears every geofence and stops any sessions still running:
Task {
await DoorstepAI.stopRouteGeofencing()
}
Update Stops
Call updateRouteStops when the driver's route changes mid-shift (a stop added, removed, or reordered). It re-diffs against the currently monitored stops instead of tearing everything down, and auto-stops a session for any stop dropped from the list, tagged removed_from_route:
Task {
do {
try await DoorstepAI.updateRouteStops(stops: updatedRouteStops) // [DeliveryStop]
} catch {
// Handle error
}
}
Resuming After Relaunch
Not part of the standard flow above, but worth adding for reliability: route state is persisted, so geofencing survives process termination and OS restarts. Call resumeRouteGeofencingIfNeeded() on every app launch to restore it without re-supplying the stop list. It's a no-op if no route is active, so it's safe to call unconditionally:
@main
struct MyApp: App {
init() {
DoorstepAI.setApiKey(key: "YOUR_API_KEY_HERE")
DoorstepAI.resumeRouteGeofencingIfNeeded()
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
Getting Monitored Stops
Read monitoredStops to get the stops currently registered, useful for rehydrating your UI on launch or confirming a route is active:
let stops = DoorstepAI.monitoredStops
print("\(stops.count) stop(s) currently monitored")
Types
DeliveryStop
A single stop on the route. deliveryId is the same id you pass to markDropoff and stopDelivery.
struct DeliveryStop {
let deliveryId: String
let address: String
let latitude: Double
let longitude: Double
let radiusMeters: Double?
let customerId: String?
let driverId: String?
}
| Field | Type | Required | Description |
|---|---|---|---|
deliveryId | String | Yes | Your id for the stop. Pass the same id to markDropoff/stopDelivery. Duplicates are de-duped (last wins) |
address | String | Yes | Passed verbatim to session creation |
latitude | Double | Yes | Stop latitude (geofence center) |
longitude | Double | Yes | Stop longitude (geofence center) |
radiusMeters | Double? | No | Per-stop geofence radius. Falls back to RouteGeofenceOptions.defaultRadiusMeters / remote config when nil |
customerId | String? | No | Forwarded to session creation for correlation on your backend |
driverId | String? | No | Forwarded to session creation for correlation on your backend |
RouteGeofenceOptions
Tuning for the whole route. Every field has a default, so startRouteGeofencing(stops: stops) works with no options.
struct RouteGeofenceOptions {
var defaultRadiusMeters: Double = 250
var timeoutSeconds: Int? = nil
var autoStopAfterDropoffSeconds: Int? = nil
}
| Field | Type | Default | Description |
|---|---|---|---|
defaultRadiusMeters | Double | 250 | Geofence radius for stops that don't set their own radiusMeters |
timeoutSeconds | Int? | nil | Backstop for each auto-started session: it stops this long after starting even if no EXIT/dropoff arrives. nil uses remote config |
autoStopAfterDropoffSeconds | Int? | nil | Forwarded to the post-dropoff auto-stop, same as startDelivery*'s autoStopAfterDropoffSeconds |
GeofenceSessionEvent & GeofenceSessionEventType
Emitted whenever the SDK auto-starts or auto-stops a session in response to a geofence transition.
struct GeofenceSessionEvent {
let deliveryId: String
let type: GeofenceSessionEventType
let reason: String
}
enum GeofenceSessionEventType {
case started
case stopped
}
| Field | Type | Description |
|---|---|---|
deliveryId | String | The stop's DeliveryStop.deliveryId |
type | GeofenceSessionEventType | .started (session auto-started on ENTER) or .stopped (session auto-stopped) |
reason | String | What triggered it: geofence, distance, timeout, removed_from_route, route_cleared, or manual |
Observing Geofencing Events
Subscribe with addGeofenceSessionListener to receive auto start/stop events so your UI can reflect them in real time. It returns a subscription; call .remove() to unsubscribe:
let subscription = DoorstepAI.addGeofenceSessionListener { event in
print("Geofencing event: \(event.deliveryId) -> \(event.type) (\(event.reason))")
}
// Later, e.g. in deinit:
subscription.remove()
Manual Geofencing
If you'd rather drive geofence enter/exit yourself instead of using In-SDK Geofencing above, wire up these four touchpoints:
The DoorstepAI SDK has four touchpoints in your driver app. Wire up each one and you're done. Everything else on this page is reference detail for those four calls.
| # | When this happens in the driver app | What you call |
|---|---|---|
| 1 | App launches | Initialize SDK |
| 2 | Driver enters the ≥250 m delivery geofence | Start Tracking |
| 3 | Driver takes POD or confirms drop-off in-app | Mark Drop-off |
| 4 | Driver exits the ≥250 m delivery geofence | Stop Tracking |
1. Initialize SDK
Initialize the SDK the same way as described in Initialize SDK above. This step is identical for both flows.
2. Start Tracking
Call startDelivery… when the driver enters the delivery geofence around the target building, not once inside it. Use a unique deliveryId per session: a meaningful identifier you can correlate on your side. Wrap calls in do/catch to handle failures such as an invalid key or denied permissions.
Pick whichever address format you have (startDeliveryByPlaceID, startDeliveryByAddressType, or startDeliveryByAddressString):
Task {
do {
// By Google Place ID
try await DoorstepAI.startDeliveryByPlaceID(
placeID: "some_place_id",
deliveryId: "delivery_12345"
)
// By address components
let address = AddressType(
streetNumber: "123",
route: "Main Street",
subPremise: "Apt 4B",
locality: "San Francisco",
administrativeAreaLevel1: "CA",
postalCode: "94102"
)
try await DoorstepAI.startDeliveryByAddressType(
address: address,
deliveryId: "delivery_12345"
)
// By single address string, with optional coordinates and knobs
try await DoorstepAI.startDeliveryByAddressString(
address: "123 Main St, Apt 4B, San Francisco, CA 94102",
deliveryId: "delivery_12345",
coordinates: LatLngObject(lat: 37.7749, lng: -122.4194),
timeoutSeconds: 1200
)
} catch {
// Handle invalid key, denied permissions, or bad input
}
}
Start parameters
Every start method takes a deliveryId plus these optional knobs:
| Parameter | Type | Description |
|---|---|---|
deliveryId | String | Required. Unique per session; correlate it on your side |
timeoutSeconds | TimeInterval? | Auto-stops tracking after this duration, a backstop if the exit geofence is missed |
coordinates | LatLngObject? | (AddressType / address-string variants only) pairs a textual address with a lat/lng you resolved upstream |
customerId | String? | Optional customer identifier passed through to session creation, for correlation on your backend |
driverId | String? | Optional driver identifier passed through to session creation, for correlation on your backend |
Deprecated start methods
startDeliveryByPlusCode and startDeliveryByLatLng are deprecated. Use startDeliveryByAddressString / startDeliveryByAddressType with coordinates instead. They remain for backwards compatibility:
try await DoorstepAI.startDeliveryByPlusCode(
plusCode: "some_plus_code",
deliveryId: "delivery_12345",
timeoutSeconds: 1200
)
try await DoorstepAI.startDeliveryByLatLng(
latitude: 37.7749,
longitude: -122.4194,
subUnit: "Apt 4B",
deliveryId: "delivery_12345",
timeoutSeconds: 1200
)
3. Mark Drop-off
Mark the drop-off the same way as described in Mark Drop-off above. This step is identical for both flows. newEvent there also accepts an optional timestamp (epoch seconds) to backdate a custom event; omit it to use the current time.
4. Stop Tracking
Call stopDelivery when the driver exits the delivery geofence surrounding the building, not while inside it:
Task {
await DoorstepAI.stopDelivery(deliveryId: "delivery_12345")
}
That covers the full lifecycle. The sections below are reference for the calls above.
Advanced APIs
Forward background URLSession completion handlers
The SDK owns background upload sessions. Forward the handler from your app delegate together with the session identifier so iOS can finish the correct session:
func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
DoorstepAI.setBackgroundSessionCompletionHandler(
completionHandler,
forSessionIdentifier: identifier
)
}
The identifier-less setBackgroundSessionCompletionHandler(_:) overload remains supported for
existing integrations, but the identifier form is preferred because the SDK owns more than one
background URL session.
SwiftUI root component (legacy)
You can render the SDK root component for legacy support. This is no longer required for new integrations; tracking starts when you call startDelivery*.
import SwiftUI
import DoorstepDropoffSDK
struct ContentView: View {
var body: some View {
VStack {
Text("Welcome to My Delivery App")
DoorstepAIRoot() // optional legacy root component
}
}
}
Best Practices
- Always handle errors. Wrap
startDelivery*/markDropoffindo/catchand surface invalid-key, denied-permission, and bad-input failures to the user. - Update UI on the main thread. SDK calls run in
Tasks; hop back to the main actor before touching UI. - Let tracking run in the background. The SDK handles backgrounding automatically, so don't stop a delivery just because the app is backgrounded. Only call
stopDeliveryon geofence exit (or rely ontimeoutSecondsas a backstop).
Next Steps
- 🔐 Review Permissions: buckets, states, and staged prompts
- 💡 View Examples: complete implementation examples with error handling