Skip to Content

iOS SDK: Surviving Suspension

Apple’s CoreLocation framework provides powerful tools for background tracking, but iOS is extremely aggressive at suspending apps to save memory and battery life.

This page explains exactly how Tracelet interacts with Apple’s strict background execution policies, and how you can configure it for your specific scenario.


Permissions & Info.plist Setup

Apple enforces strict privacy requirements. To use Tracelet effectively, you must declare exactly why you need certain permissions in your ios/Runner/Info.plist. Apple will reject your app during App Store review if these strings are missing or don’t clearly explain the use-case.

1. Required Usage Descriptions

Add the following keys to your Info.plist:

<!-- Required for basic tracking --> <key>NSLocationWhenInUseUsageDescription</key> <string>We need your location to track your route while the app is open.</string> <!-- Required for background tracking when the app is minimized or killed --> <key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <string>We need background location to record your route even when the app is closed.</string> <!-- Required for the smart motion-detection battery saving engine --> <key>NSMotionUsageDescription</key> <string>Motion detection allows battery-efficient tracking by pausing GPS when stationary.</string>

2. Background Modes

For Tracelet to run in the background (and execute headless Dart code when the app is killed), you must declare the proper Background Modes capabilities in Xcode.

Alternatively, add them directly to your Info.plist:

<key>UIBackgroundModes</key> <array> <string>location</string> <string>fetch</string> <!-- Only add 'audio' if you are using 'preventSuspend: true' (See Scenario 1 below) --> </array>

3. Removing Optional Features (No Crashes)

If your client doesn’t want specific features (e.g., they don’t want to track motion or activity types), you can safely omit the corresponding key from your Info.plist. Tracelet’s native Swift code safely checks for key presence before requesting features.

Feature KeyEffect when removed from Info.plist
NSMotionUsageDescriptionSafe. iOS will silently fall back to checking location changes for movement. CMMotionActivityManager won’t be invoked, and the user won’t be prompted for Motion permissions.
UIBackgroundModes -> audioSafe. Only required if your config sets preventSuspend: true to prevent Dart isolate suspension.
NSLocationAlwaysAndWhenInUseUsageDescriptionSafe. The app will only ever track while in the foreground (or temporarily via the blue pill). Tracelet handles the graceful degradation internally.

Scenario 1: The Fitness App (High Accuracy, No Suspend)

Concepts Explored: Activity Types, Suspension, Audio Wake

The Problem

Your user is running a marathon. They open your app, start the run, and lock their iPhone. Five minutes later, iOS decides it needs RAM to run a background iCloud backup, so it completely suspends your app’s Dart isolate. Tracelet’s native swift code continues to gather GPS points, but because the Dart isolate is frozen, your Flutter UI stops updating the distance counter, and any live-tracking web-sockets you have running in Dart are killed.

How Tracelet Solves It: Prevent Suspend

To keep the Flutter Dart isolate running indefinitely while the screen is off, you must trick iOS into thinking the app is actively playing media.

ios: tl.IosConfig( activityType: tl.LocationActivityType.fitness, // Optimizes GPS filtering for running preventSuspend: true, )

Setting preventSuspend: true causes Tracelet to play an imperceptible, silent audio clip on a loop. Because iOS thinks the user is listening to music, it will NEVER suspend the Dart isolate.

App Store Review Note: Using preventSuspend requires the audio background mode capability in Xcode. Apple will reject your app if you use background audio purely to keep the app alive without a legitimate user-facing reason (e.g., a fitness tracker playing voice cues, or a navigation app speaking turn-by-turn directions). Do not use this for silent fleet tracking!

To enable this: Open your iOS project in Xcode, go to Signing & Capabilities -> + Capability -> Background Modes -> check Audio, AirPlay, and Picture in Picture.


Scenario 2: The Social Radar (Low Power, Coarse)

Concepts Explored: Significant Changes, Authorization Levels

The Problem

You are building a social networking app that notifies the user when a friend is nearby. You don’t need turn-by-turn accuracy; you just need to know roughly what neighborhood they are in. You also don’t want to show the scary “Always Allow” permission prompt, as users will reject it.

How Tracelet Solves It: Significant Changes

Instead of powering up the GPS chip, Tracelet can rely entirely on cell-tower handoffs.

ios: tl.IosConfig( useSignificantChangesOnly: true, locationAuthorizationRequest: tl.LocationAuthorizationRequest.whenInUse, disableLocationAuthorizationAlert: true, )
  1. Significant Changes Only: By enabling this, Tracelet tells iOS to only wake the app up when the device jumps to a completely different cell tower (usually 500m to several kilometers). The battery drain is practically zero.
  2. When In Use Authorization: You only ask for “When In Use” permission. Tracelet respects this and will not trigger the native Apple prompt telling the user to go to Settings to enable “Always”. (To learn how to trigger these permission prompts from Dart, see the Flutter SDK: Permissions page).

Scenario 3: The Blue Location Indicator (showsBackgroundLocationIndicator)

Concepts Explored: Background Location Indicator

What this flag actually does

showsBackgroundLocationIndicator maps directly to Apple’s CLLocationManager.showsBackgroundLocationIndicator. The name is counter‑intuitive — it is an opt‑in to show the indicator, not a switch to hide it:

ValueMeaning
trueShow the blue status‑bar pill / Dynamic Island indicator while the app uses location in the background.
false (default)Request to hide the indicator for background location use.

Common misconception: setting showsBackgroundLocationIndicator: true does not turn the blue indicator off — it turns it on. If your goal is to hide it, leave it false (the default). Setting it true is only useful when you want the pill visible (e.g. to satisfy “When In Use” background tracking, below).

When you want the pill (temporary background tracking on “When In Use”)

You only have “When In Use” permission but want to keep tracking while the user completes a task (e.g. a rideshare pickup). iOS permits this only if the indicator is visible, so the user knows tracking is happening:

ios: tl.IosConfig( showsBackgroundLocationIndicator: true, )

To enable this: the location Background Mode must be on in Xcode (Signing & Capabilities → Background Modes → Location updates).

Why false doesn’t always hide it

Even with showsBackgroundLocationIndicator: false, iOS forces the indicator on in these cases — the flag cannot override them:

  1. “When In Use” authorization — background location always shows the indicator. Only full “Always” authorization can suppress it.
  2. useBackgroundActivitySession: true (iOS 17+) — Apple requires the indicator while a CLBackgroundActivitySession is active (Scenario 4).
  3. Live Activities active (Scenario 5).
  4. Any continuous background location session — if the app is running startUpdatingLocation non‑stop, the indicator stays on regardless of the flag.

To actually keep the indicator hidden

  1. Obtain full “Always” authorization (note iOS may first grant a provisional “When In Use” until the user later confirms “Change to Always” — the pill appears until then).
  2. Keep showsBackgroundLocationIndicator: false (don’t set it to true).
  3. Avoid unnecessary continuous background location. Geofence‑only mode uses native region monitoring (no continuous GPS), so it shows no indicatorstartGeofences() does not run continuous updates in standard mode.

Scenario 4: The Dynamic Island Indicator (iOS 17+)

Concepts Explored: Background Activity Session

The Problem

You want the benefits of the blue pill indicator, but you want a more modern, native integration with the Dynamic Island on modern iPhones, and you want to reduce the chances of the OS killing your background session.

How Tracelet Solves It

Tracelet integrates with Apple’s CLBackgroundActivitySession (introduced in iOS 17). This provides a prominent Dynamic Island indicator and creates a formal session with the OS, telling it not to suspend your app.

ios: tl.IosConfig( useBackgroundActivitySession: true, )

App Store Review Requirements: Apple explicitly requires a clear explanation of why your app needs persistent background location. If you use CLBackgroundActivitySession, you must provide justification in three places, or your app will be rejected:

  1. App Store Connect Review Notes: You must provide a clear written explanation to the reviewer about why the app needs this feature, along with a link to a demo video showing the feature in action.
  2. App Store Description: Your public app description must clearly state that the app uses background location (e.g., “This app uses background location to track your runs even when the app is closed.”).
  3. In-App Onboarding: Before requesting location permissions, your app’s UI must clearly explain to the user why background location is needed.

Scenario 5: Live Activities (Lock Screen & Dynamic Island UI)

Concepts Explored: ActivityKit, Lock Screen Widgets, Dynamic Island

The Problem

While tracking in the background on iOS 17+, you want a rich, glanceable indicator on the Lock Screen and Dynamic Island — so the user always knows tracking is active — instead of just the small blue location pill.

How Tracelet Solves It

Tracelet integrates with Apple’s ActivityKit. If you provide a liveActivityConfig and add a Widget Extension, Tracelet automatically starts a Live Activity when tracking starts and ends it when tracking stops.

The Live Activity is a UI layer on top of Tracelet’s standard background pipeline. Battery efficiency itself comes from the motion-detection engine pausing GPS when stationary and the background-session integration (Scenario 4) — not from the activity widget. Tracelet does not open a second CLLocationUpdate.liveUpdates() stream, which would duplicate GPS work.

ios: tl.IosConfig( liveActivityConfig: tl.LiveActivityConfig( title: 'Ride in progress', body: 'Tracking your route to the destination...', ), )

Refreshing the Live Activity While Tracking (updateNotification())

To change what the Live Activity shows after tracking has started, update liveActivityConfig via setConfig() and then call Tracelet.updateNotification(). Since v3.6.8 this refreshes the Live Activity from the latest config without restarting the tracking pipeline — the cross-platform counterpart to refreshing the Android foreground-service notification:

await Tracelet.setConfig( const tl.Config( ios: tl.IosConfig( liveActivityConfig: tl.LiveActivityConfig( title: 'Ride in progress', body: 'Arriving in 2 minutes', ), ), ), ); await Tracelet.updateNotification();
ℹ️

Only the body is updated on a running activity — the title lives in the immutable ActivityAttributes and cannot change without ending and re-requesting the activity (an ActivityKit constraint).

The Live Activity is bound to the moving sub-state (it is dismissed when Tracelet pauses GPS on going stationary), so it may not be on screen at the moment you refresh it. While a tracking session is active, updateNotification() handles this for you: it updates the running activity in place, or re-presents it with the latest content if it was dismissed. It is a safe no-op when no liveActivityConfig is set or tracking is stopped.

Xcode Widget Extension Setup (Required)

What is this for? This setup allows your app to display a Live Activity on the iOS Lock Screen and Dynamic Island while tracking is active. It provides users with glanceable information about their ongoing session without opening the app.

Does it save battery? No. The Live Activity is purely a UI layer and does not save battery. Battery efficiency comes entirely from Tracelet’s core background engine (motion detection, pausing GPS when stationary, and background sessions).

Is it mandatory? No. This setup is completely optional. If you skip this, Tracelet will still track perfectly in the background, but users will only see standard system indicators (like the blue location pill) instead of your custom UI.

Unlike Android, Flutter plugins cannot dynamically create iOS widgets. To enable Live Activities, you must add a Widget Extension target to your iOS app in Xcode:

  1. Open ios/Runner.xcworkspace in Xcode.
  2. Go to File -> New -> Target… and select Widget Extension.
  3. Name it TraceletWidget. Make sure Include Live Activity is checked.
  4. In BOTH your app’s Info.plist AND your new Widget Extension’s Info.plist (right click them in Xcode -> Open As -> Source Code), you must add the following key inside the main <dict>:
<key>NSSupportsLiveActivities</key> <true/>
  1. Sync the Extension Version (only matters for App Store submission): Apple rejects an app extension whose CFBundleShortVersionString / CFBundleVersion don’t match the host app. Set the widget target’s Version and Build to match your app: select the TraceletWidget target → GeneralIdentity, and set Version to your app’s version and Build to your app’s build number (the same values as your pubspec.yaml).

Do not try to set these by pasting $(FLUTTER_BUILD_NAME) / $(FLUTTER_BUILD_NUMBER) into the widget’s Info.plist. Those variables are only defined for the Runner target (via Flutter’s Generated.xcconfig); in the widget target they resolve to empty, so the versions silently won’t match. Use the target’s build settings (MARKETING_VERSION / CURRENT_PROJECT_VERSION) as above. This is a submission requirement only — a mismatch does not crash the app at runtime.

  1. Do NOT Link Flutter Dependencies: Do not link FlutterGeneratedPluginSwiftPackage or the Flutter engine to your Widget Extension. Doing so causes a launch-time dyld crash (Library not loaded) in Release mode, because Flutter does not embed its dynamic SPM frameworks into App Extensions.
  2. Replace the auto-generated TraceletWidgetLiveActivity.swift content with the following. Notice that we manually define the TraceletActivityAttributes struct here instead of importing the SDK — ActivityKit matches the activity by the struct’s (unqualified) name and shape, so this keeps your Widget lightweight and avoids linking the SDK into the extension:
import ActivityKit import WidgetKit import SwiftUI // Define the exact struct expected by Tracelet's native core public struct TraceletActivityAttributes: ActivityAttributes { public struct ContentState: Codable, Hashable { public var status: String public init(status: String) { self.status = status } } public var title: String public init(title: String) { self.title = title } } @main struct TraceletWidgetBundle: WidgetBundle { var body: some Widget { TraceletWidgetLiveActivity() } } struct TraceletWidgetLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: TraceletActivityAttributes.self) { context in // Lock Screen / banner UI — fully self-contained (no SDK import needed) HStack(spacing: 12) { Image(systemName: "location.fill").foregroundColor(.blue) VStack(alignment: .leading) { Text(context.attributes.title).font(.headline) Text(context.state.status).font(.subheadline).foregroundColor(.secondary) } Spacer() } .padding() } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.leading) { Text("Tracking") } DynamicIslandExpandedRegion(.trailing) { Text("Live") } DynamicIslandExpandedRegion(.bottom) { Text(context.state.status) } } compactLeading: { Image(systemName: "location.fill").foregroundColor(.blue) } compactTrailing: { Text("Live") } minimal: { Image(systemName: "location.fill").foregroundColor(.blue) } } } }

This SwiftUI view is fully yours to customize — style it however you like inside the ActivityConfiguration block, mapping the data from context.attributes.title and context.state.status.

Set the Widget Extension’s deployment target to iOS 16.2. Xcode’s “Widget Extension” template often adds Control Widgets and an @available(iOS 18.0, *) annotation on the @main WidgetBundle. If that annotation is higher than the extension’s deployment target, the widget bundle (and therefore your Live Activity) silently fails to register — the system logs Activity had no descriptor — and the extension can crash on older OS versions. Keep the @main bundle available at the extension’s deployment floor, and wrap any iOS-18-only widgets (e.g. Control Widgets) in if #available(iOS 18.0, *).


Scenario 6: The Terminated State (App Force-Quit)

Concepts Explored: App Lifecycle, Force-Quit vs OS Suspension

The Problem

Your user swipes up from the app switcher and completely force-quits the app. You expect Tracelet to continue tracking them in the background, but the locations stop coming in.

How Apple Handles It

Unlike Android, where a Foreground Service can often survive a swipe-to-dismiss, iOS strictly enforces user intent.

According to Apple’s Official CoreLocation Documentation :

“If the user force-quits your app, the system does not automatically launch it when new location events arrive. The user must explicitly relaunch your app before the system resumes delivering location events.”

When a user manually force-quits the app:

  1. Standard Location Updates: Stopped completely.
  2. Significant Location Changes (SLC): Stopped completely.
  3. Region Monitoring (Geofences): Stopped completely.

Will it work in the Terminated State?

  • If the OS terminated the app (e.g., due to memory pressure in the background): Yes. Apple will automatically relaunch your app in the background when a location event occurs (like a significant change or geofence trigger). Tracelet’s native Rust core will wake up and handle it seamlessly.
  • If the user explicitly force-quits the app: No. The app will remain dead until the user manually taps the app icon to open it again.

How Tracelet Solves It

Tracelet cannot bypass Apple’s fundamental OS constraints. However, Tracelet ensures that:

  1. All unsynced locations are safely persisted in the Rust SQLite database before the app is killed.
  2. As soon as the user opens the app again, Tracelet instantly resumes tracking and syncs any offline data without missing a beat.
Last updated on