Skip to Content

iOS SDK:幸存的暂停

Apple 的 CoreLocation 框架提供了强大的后台跟踪工具,但 iOS 在暂停应用程序以节省内存和电池寿命方面非常积极。

本页准确解释了 Tracelet 如何与 Apple 严格的后台执行策略交互,以及如何针对您的特定场景配置它。


权限和 Info.plist 设置

Apple 执行严格的隐私要求。要有效地使用 Tracelet,您必须在 ios/Runner/Info.plist 中准确声明为什么您需要某些权限。如果这些字符串丢失或没有清楚地解释用例,Apple 将在 App Store 审核期间拒绝您的应用程序。

1. 所需使用说明

将以下键添加到您的 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. 背景模式

为了让 Tracelet 在后台运行(并在应用程序被终止时执行 headless Dart 代码),您必须在 Xcode 中声明正确的后台模式功能。

或者,将它们直接添加到您的 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. 删除可选功能(无崩溃)

如果您的客户不需要特定功能(例如,他们不想跟踪运动或活动类型),您可以安全地从 Info.plist 中省略相应的密钥。 Tracelet 的本机 Swift 代码会在请求功能之前安全地检查密钥是否存在。

功能键Info.plist 中删除时的效果
不翻译迟到安全。 iOS 将默默地回退到检查移动的位置变化。不会调用 CMMotionActivityManager,并且不会提示用户输入 Motion 权限。
不翻译迟到 -> 不翻译迟到安全。 仅当您的配置设置 preventSuspend: true 以防止 Dart 隔离挂起时才需要。
不翻译迟到安全。 该应用程序只会在前台进行跟踪(或暂时通过蓝色药丸进行跟踪)。 Tracelet 在内部处理优雅降级。

场景一:健身App(高精度、无暂停)

探索的概念: 活动类型、暂停、音频唤醒

问题

您的用户正在跑马拉松。他们打开您的应用程序,开始跑步,然后锁定他们的 iPhone。五分钟后,iOS 决定需要 RAM 来运行后台 iCloud 备份,因此它完全暂停了应用程序的 Dart 隔离。 Tracelet 的本机 swift 代码继续收集 GPS 点,但由于 Dart 隔离被冻结,您的 Flutter UI 停止更新距离计数器,并且您在 Dart 中运行的任何实时跟踪 Web 套接字都将被终止。

Tracelet 如何解决这个问题:防止挂起

为了在屏幕关闭时保持 Flutter Dart 隔离无限期运行,您必须欺骗 iOS 认为该应用程序正在主动播放媒体。

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

设置 preventSuspend: true 会使 Tracelet 循环播放难以察觉的无声音频剪辑。因为 iOS 认为用户正在听音乐,所以它永远不会暂停 Dart 隔离。

App Store 评论注意: 使用 preventSuspend 需要 Xcode 中的 audio 后台模式功能。如果您纯粹使用背景音频来保持应用程序运行,而没有面向用户的合法原因(例如,健身追踪器播放语音提示,或导航应用程序说出逐段指示),Apple 将拒绝您的应用程序。请勿将其用于无声车队跟踪!

要启用此功能: 在 Xcode 中打开您的 iOS 项目,转到 签名和功能 -> + 功能 -> 背景模式 -> 检查 音频、AirPlay 和画中画


场景 2:社交雷达(低功耗、粗略)

探索的概念: 重大变化、授权级别

问题

您正在构建一个社交网络应用程序,当朋友在附近时通知用户。您不需要逐个转弯的准确性;您只需要大致了解他们所在的社区。您也不想显示可怕的“始终允许”权限提示,因为用户会拒绝它。

Tracelet 如何解决这个问题:重大变化

Tracelet 可以完全依赖手机信号塔的切换,而不是给 GPS 芯片供电。

ios: tl.IosConfig( useSignificantChangesOnly: true, locationAuthorizationRequest: tl.LocationAuthorizationRequest.whenInUse, disableLocationAuthorizationAlert: true, )
  1. 仅重大更改: 通过启用此功能,Tracelet 告诉 iOS 仅在设备跳转到完全不同的手机信号塔(通常为 500m 到几公里)时唤醒应用程序。电池消耗几乎为零。
  2. 使用时授权: 您只需请求“使用时”权限。 Tracelet 尊重这一点,并且不会触发本机 Apple 提示,告诉用户前往“设置”以启用“始终”。 (要了解如何从 Dart 触发这些权限提示,请参阅 Flutter SDK:权限 页面)。

场景 3:蓝色位置指示器 (showsBackgroundLocationIndicator)

探索的概念: 背景位置指示器

这个标志实际上做了什么

showsBackgroundLocationIndicator 直接映射到 Apple 的 CLLocationManager.showsBackgroundLocationIndicator。这个名字是反直觉的——它是一个选择“显示”指示器,而不是隐藏它的开关

价值意义
不翻译迟到当应用程序在后台使用位置时,显示蓝色状态栏药丸/动态岛指示器。
false (默认)请求隐藏后台位置使用的指示器。

**常见误解:设置 showsBackgroundLocationIndicator: true 不会将蓝色指示灯关闭 — 而是将其打开。如果您的目标是隐藏它,请将其保留为 false(默认值)。设置 true 仅当您“希望”药丸可见时才有用(例如,满足下面的“使用时”背景跟踪)。

当您“想要”药丸时(“使用时”的临时背景跟踪)

您只有“使用时”权限,但希望在用户完成任务(例如拼车接送)时继续跟踪。 iOS 仅当指示器可见时才允许这样做,因此用户知道正在发生跟踪:

ios: tl.IosConfig( showsBackgroundLocationIndicator: true, )

要启用此功能: Xcode 中必须启用 location 后台模式(签名和功能 → 后台模式 → 位置更新)。

为什么 false 并不总是隐藏它

即使使用 showsBackgroundLocationIndicator: false,在这些情况下,iOS 也会强制打开指示器 — 该标志无法覆盖它们:

  1. “使用时”授权 — 背景位置始终显示指示器。只有完全**“始终”**授权才能抑制它。
  2. useBackgroundActivitySession: true (iOS 17+) — 当 CLBackgroundActivitySession 处于活动状态时,Apple 需要该指示器(场景 4)。
  3. 现场活动 活跃(场景 5)。
  4. 任何连续的后台位置会话 — 如果应用程序正在不停地运行 startUpdatingLocation,则无论标志如何,该指示器都会保持亮起状态。

实际上隐藏指示器

  1. 获得完整的**“始终”**授权(请注意,iOS 可能首先授予临时“使用时”权限,直到用户稍后确认“更改为始终” - 药丸会在此之前出现)。
  2. 保留showsBackgroundLocationIndicator: false(不要将其设置为true)。 3.避免不必要的连续背景位置。 仅地理围栏模式使用本机区域监控(无连续 GPS),因此不显示任何指示符startGeofences() 在标准模式下不会运行连续更新。

场景 4:动态孤岛指示器(iOS 17+)

概念探讨: 后台活动会议

问题

您想要蓝色药丸指示器的好处,但您想要与现代 iPhone 上的动态岛进行更现代、原生的集成,并且您希望减少操作系统杀死后台会话的机会。

Tracelet 如何解决这个问题

Tracelet 与 Apple 的 CLBackgroundActivitySession 集成(在 iOS 17 中引入)。这提供了一个显着的动态岛指示器,并创建与操作系统的正式会话,告诉它不要暂停您的应用程序。

ios: tl.IosConfig( useBackgroundActivitySession: true, )

应用程序商店审核要求: Apple 明确要求明确解释为什么您的应用程序需要持久的后台位置。如果您使用 CLBackgroundActivitySession,您必须在三个地方提供理由,否则您的应用程序将被拒绝:

  1. App Store Connect 审核说明: 您必须向审核者提供关于“为什么”应用程序需要此功能的清晰书面解释,以及显示该功能实际操作的演示视频的链接。
  2. 应用程序商店描述: 您的公共应用程序描述必须明确说明该应用程序使用后台位置(例如,“即使应用程序关闭,此应用程序也使用后台位置来跟踪您的跑步情况。”)。
  3. 应用程序内入门: 在请求位置权限之前,您的应用程序的 UI 必须向用户清楚地解释为什么需要后台位置。

场景五:直播活动(锁屏&动态岛UI)

探索的概念: ActivityKit、锁屏小部件、动态岛

问题

在 iOS 17+ 上进行后台跟踪时,您需要在锁定屏幕和动态岛上有一个丰富、一目了然的指示器,以便用户始终知道跟踪处于活动状态,而不仅仅是蓝色的小定位药丸。

Tracelet 如何解决这个问题

Tracelet 与 Apple 的 ActivityKit 集成。如果您提供 liveActivityConfig 并添加小部件扩展,则 Tracelet 会在跟踪开始时自动启动实时活动,并在跟踪停止时结束活动。

Live Activity 是位于 Tracelet 标准后台管道之上的 UI 层。电池效率本身来自于静止时暂停 GPS 的运动检测引擎和后台会话集成(场景 4),而不是来自活动小部件。 Tracelet 不会**打开第二个 CLLocationUpdate.liveUpdates() 流,这会重复 GPS 工作。

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

跟踪时刷新实时活动 (updateNotification())

要更改实时活动在跟踪开始后显示的内容,请通过 setConfig() 更新 liveActivityConfig,然后调用 Tracelet.updateNotification()。从 v3.6.8 开始,这会从最新配置刷新实时活动 而无需重新启动跟踪管道 — 刷新 Android 前台服务通知 的跨平台对应项:

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

只有 body 在正在运行的活动上更新 - title 存在于不可变的 ActivityAttributes 中,并且在不结束并重新请求活动(ActivityKit 约束)的情况下无法更改。

实时活动绑定到移动子状态(当 Tracelet 在静止状态下暂停 GPS 时,它会被解除),因此在您刷新它时它可能不在屏幕上。当跟踪会话处于活动状态时,updateNotification() 会为您处理此问题:它会就地更新正在运行的活动,或者在被取消时使用最新内容重新呈现。当未设置 liveActivityConfig 或跟踪停止时,这是安全的无操作。

Xcode Widget 扩展设置(必需)

这是做什么用的? 此设置允许您的应用程序在跟踪处于活动状态时在 iOS 锁定屏幕和动态岛上显示实时活动。它为用户提供了有关其正在进行的会话的一目了然的信息,而无需打开应用程序。

它可以节省电池吗? 不。 Live Activity 纯粹是 UI 层,不节省电池。电池效率完全来自 Tracelet 的核心后台引擎(运动检测、静止时暂停 GPS 和后台会话)。

这是强制性的吗? 不。 此设置完全是可选的。如果您跳过此步骤,Tracelet 仍将在后台完美跟踪,但用户只会看到标准系统指示器(如蓝色位置药丸),而不是您的自定义 UI。

与 Android 不同,Flutter 插件无法动态创建 iOS 小部件。要启用实时活动,您必须在 Xcode 中将 Widget Extension 目标添加到您的 iOS 应用程序:

  1. 在 Xcode 中打开 ios/Runner.xcworkspace
  2. 转至 文件 -> 新建 -> 目标… 并选择 小部件扩展
  3. 将其命名为“TraceletWidget”。确保选中包括实时活动
  4. 在您​​的应用程序的 Info.plist 和新的小部件扩展的 Info.plist 中(在 Xcode -> 打开方式 -> 源代码中右键单击它们),您必须在主 <dict> 中添加以下键:
<key>NSSupportsLiveActivities</key> <true/>
  1. 同步扩展版本(仅适用于 App Store 提交): Apple 会拒绝 CFBundleShortVersionString / CFBundleVersion 与主机应用程序不匹配的应用程序扩展。设置小部件目标的 VersionBuild 以匹配您的应用程序:选择 TraceletWidget 目标 → GeneralIdentity,并将 Version 设置为您应用程序的版本,将 Build 设置为您应用程序的内部版本号(与您的 pubspec.yaml 的值相同)。

不要尝试通过将 $(FLUTTER_BUILD_NAME) / $(FLUTTER_BUILD_NUMBER) 粘贴到小部件的 Info.plist 中来设置这些。这些变量仅为 Runner 目标定义(通过 Flutter 的 Generated.xcconfig);在小部件目标中,它们解析为空,因此版本将不匹配。如上所述使用目标的构建设置(MARKETING_VERSION / CURRENT_PROJECT_VERSION)。这只是一个提交要求——不匹配不会在运行时使应用程序崩溃。

  1. 不要链接 Flutter 依赖项: 不要将 FlutterGeneratedPluginSwiftPackage 或 Flutter 引擎链接到您的 Widget 扩展。这样做会导致发布模式下启动时 dyld 崩溃 (Library not loaded),因为 Flutter 不会将其动态 SPM 框架嵌入到应用程序扩展中。
  2. 将自动生成的 TraceletWidgetLiveActivity.swift 内容替换为以下内容。请注意,我们在这里手动定义 TraceletActivityAttributes 结构,而不是导入 SDK — ActivityKit 通过结构的(非限定)名称和形状来匹配活动,因此这可以使您的 Widget 保持轻量级并避免将 SDK 链接到扩展中:
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) } } } }

这个 SwiftUI 视图完全由您自定义 - 在 ActivityConfiguration 块内按照您喜欢的方式设计它,映射来自 context.attributes.titlecontext.state.status 的数据。

将小部件扩展的部署目标设置为 iOS 16.2。 Xcode 的“小部件扩展”模板通常会在 @main WidgetBundle 上添加控制小部件和 @available(iOS 18.0, *) 注释。如果该注释高于扩展程序的部署目标,则小部件包(以及您的实时活动)会默默地无法注册 - 系统记录 Activity had no descriptor - 并且扩展程序可能会在较旧的操作系统版本上崩溃。保持 @main 捆绑包在扩展的部署层可用,并将任何仅限 iOS-18 的小部件(例如控制小部件)包装在 if #available(iOS 18.0, *) 中。


场景 6:终止状态(应用程序强制退出)

探索的概念: 应用程序生命周期、强制退出与操作系统暂停

问题

您的用户从应用程序切换器向上滑动并完全强制退出应用程序。您希望 Tracelet 继续在后台跟踪它们,但位置停止传入。

苹果如何处理

与 Android 不同,在 Android 中,前台服务通常可以在滑动关闭后继续存在,iOS 严格执行用户意图

根据Apple 官方 CoreLocation 文档 

“如果用户强制退出您的应用程序,当新的位置事件到达时,系统不会自动启动它。用户必须在系统恢复传递位置事件之前明确重新启动您的应用程序。”

当用户手动强制退出应用程序时:

  1. 标准位置更新: 完全停止。
  2. 重大位置变更 (SLC): 完全停止。
  3. 区域监控(地理围栏): 完全停止。

它会在终止状态下工作吗?

  • 如果操作系统终止了应用程序(例如,由于后台内存压力): 是的。当发生位置事件(例如重大变化或地理围栏触发)时,Apple 会自动在后台重新启动您的应用程序。 Tracelet 的原生 Rust 核心将唤醒并无缝处理它。
  • 如果用户明确强制退出应用程序: 否。应用程序将保持死机状态,直到用户手动点击应用程序图标再次打开它。

Tracelet 如何解决这个问题

Tracelet 无法绕过 Apple 的基本操作系统限制。然而,Tracelet 确保:

  1. 在应用程序被终止之前,所有未同步的位置都安全地保存在 Rust SQLite 数据库中。
  2. 一旦用户再次打开应用程序,Tracelet 就会立即恢复跟踪并同步任何离线数据,不会错过任何一个节拍。