iOS SDK: 存続停止
Apple の CoreLocation フレームワークはバックグラウンド追跡のための強力なツールを提供しますが、iOS はメモリとバッテリー寿命を節約するためにアプリを非常に積極的に一時停止します。
このページでは、Tracelet が Apple の厳格なバックグラウンド実行ポリシーとどのように連携するか、および特定のシナリオに合わせて Tracelet を構成する方法について正確に説明します。
権限と 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 をバックグラウンドで実行するには (アプリが終了したときにヘッドレス 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 権限を求められません。 |
UIBackgroundModes -> audio | 安全。 Dart 分離の一時停止を防ぐために設定で preventSuspend: true が設定されている場合にのみ必要です。 |
| 非トランスレート | 安全。 アプリはフォアグラウンドで (または青い錠剤を介して一時的に) のみ追跡します。 Tracelet は内部的に正常な劣化を処理します。 |
シナリオ 1: フィットネス アプリ (高精度、一時停止なし)
検討した概念: アクティビティ タイプ、サスペンション、オーディオ ウェイク
問題
ユーザーはマラソンを走っています。彼らはアプリを開いてランニングを開始し、iPhone をロックします。 5 分後、iOS はバックグラウンドで iCloud バックアップを実行するには RAM が必要であると判断し、アプリの 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,
)- 重要な変更のみ: これを有効にすると、Tracelet は、デバイスがまったく別の基地局 (通常は 500 メートルから数キロメートル) にジャンプした場合にのみアプリを起動するように iOS に指示します。バッテリーの消耗はほぼゼロです。
- 使用時の許可: 「使用時」の許可のみを要求します。 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 はインジケーターを 強制的にオンにします。フラグでインジケーターをオーバーライドすることはできません。
- 「使用時」認証 — 背景の位置に常にインジケーターが表示されます。これを抑制できるのは、完全な 「Always」 承認のみです。
useBackgroundActivitySession: true(iOS 17 以降) — Apple は、CLBackgroundActivitySessionがアクティブな間インジケーターを必要とします (シナリオ 4)。- ライブ アクティビティ がアクティブです (シナリオ 5)。
- 継続的なバックグラウンド位置情報セッション — アプリが
startUpdatingLocationをノンストップで実行している場合、インジケーターはフラグに関係なく点灯したままになります。
実際にインジケーターを非表示にしておくには
- 完全な 「常に」 承認を取得します (iOS では、ユーザーが後で「常に」に変更することを確認するまで、最初に 暫定 の「使用時」を許可する場合があります。それまでは錠剤が表示されます)。
showsBackgroundLocationIndicator: falseを維持します (trueに設定しないでください)。- 不必要な連続した背景の位置を避けます。 ジオフェンス専用モードでは、ネイティブ地域モニタリング (継続的な GPS なし) が使用されるため、インジケーターは表示されません —
startGeofences()は、標準モードでは継続的な更新を実行しません。
シナリオ 4: ダイナミック アイランド インジケーター (iOS 17 以降)
検討した概念: バックグラウンド アクティビティ セッション
問題
青い錠剤インジケーターの利点は必要ですが、最新の iPhone ではダイナミック アイランドとのより最新のネイティブ統合が必要で、OS によってバックグラウンド セッションが強制終了される可能性を減らしたいと考えています。
Tracelet がそれを解決する方法
Tracelet は、Apple の CLBackgroundActivitySession (iOS 17 で導入) と統合されています。これにより、目立つダイナミック アイランド インジケーターが提供され、OS との正式なセッションが作成され、アプリを一時停止しないように通知されます。
ios: tl.IosConfig(
useBackgroundActivitySession: true,
)App Store のレビュー要件: Apple は、アプリに永続的なバックグラウンド位置情報が必要な理由についての明確な説明を明示的に要求しています。 CLBackgroundActivitySession を使用する場合は、次の 3 か所で正当な理由を提供する必要があります。そうしないと、アプリは拒否されます。
- App Store Connect レビューのメモ: アプリにこの機能が必要な 理由 について、レビュー担当者に明確な書面による説明と、機能の動作を示すデモ ビデオへのリンクを提供する必要があります。
- App Store の説明: 公開アプリの説明には、アプリがバックグラウンド位置情報を使用することを明確に記載する必要があります (例: 「このアプリは、アプリが閉じているときでも、バックグラウンド位置情報を使用してランニングを追跡します。」)。
- アプリ内オンボーディング: 位置情報の許可をリクエストする前に、アプリの UI はバックグラウンドでの位置情報が必要な理由をユーザーに明確に説明する必要があります。
シナリオ 5: ライブ アクティビティ (ロック画面とダイナミック アイランド UI)
検討した概念: アクティビティキット、ロック画面ウィジェット、ダイナミックアイランド
問題
iOS 17 以降でバックグラウンドで追跡しているときは、小さな青い位置情報ピルだけではなく、ロック画面とダイナミック アイランドに豊富で一目でわかるインジケーターが必要です。これにより、ユーザーは常に追跡がアクティブであることがわかります。
Tracelet がそれを解決する方法
Tracelet は Apple の ActivityKit と統合されています。 liveActivityConfig を指定してウィジェット拡張機能を追加すると、Tracelet は追跡の開始時にライブ アクティビティを自動的に開始し、追跡の停止時にライブ アクティビティを終了します。
ライブ アクティビティは、Tracelet の標準バックグラウンド パイプライン上の UI レイヤーです。バッテリー効率自体は、アクティビティ ウィジェットからではなく、静止時に GPS を一時停止する動き検出エンジンとバックグラウンド セッションの統合 (シナリオ 4) から得られます。 Tracelet は、GPS の動作を重複させる 2 番目の CLLocationUpdate.liveUpdates() ストリームを開きません。
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 が設定されていない場合、または追跡が停止されている場合は、安全な no-op です。
Xcode ウィジェット拡張機能のセットアップ (必須)
これは何のためにありますか? この設定により、追跡がアクティブなときにアプリが iOS ロック画面とダイナミック アイランドにライブ アクティビティを表示できるようになります。ユーザーは、アプリを開かなくても、進行中のセッションに関する一目でわかる情報が得られます。
バッテリーの節約になりますか? いいえ。 ライブ アクティビティは純粋に UI レイヤーであり、バッテリーは節約されません。バッテリー効率は、Tracelet のコア バックグラウンド エンジン (動きの検出、静止時の GPS の一時停止、およびバックグラウンド セッション) によってもたらされます。
必須ですか? いいえ。 この設定は完全にオプションです。これをスキップしても、Tracelet は引き続きバックグラウンドで完全に追跡しますが、ユーザーにはカスタム UI ではなく、標準のシステム インジケーター (青い位置情報ピルなど) のみが表示されます。
Android とは異なり、Flutter プラグインは iOS ウィジェットを動的に作成できません。ライブ アクティビティを有効にするには、ウィジェット拡張機能 ターゲットを Xcode の iOS アプリに追加する必要があります。
- Xcode で
ios/Runner.xcworkspaceを開きます。 - ファイル -> 新規 -> ターゲット… に移動し、ウィジェット拡張機能 を選択します。
TraceletWidgetという名前を付けます。 ライブ アクティビティを含める がオンになっていることを確認します。- アプリの
Info.plistと新しいウィジェット拡張機能のInfo.plist(Xcode で右クリック -> 名前を付けて開く -> ソース コード) の両方で、メインの<dict>内に次のキーを追加する必要があります**。
<key>NSSupportsLiveActivities</key>
<true/>- 拡張機能のバージョンを同期します (App Store への送信にのみ関係します): Apple は、
CFBundleShortVersionString/CFBundleVersionがホスト アプリと一致しないアプリ拡張機能を拒否します。ウィジェット ターゲットの バージョン と ビルド をアプリと一致するように設定します。TraceletWidget ターゲット → 一般 → Identity を選択し、バージョン をアプリのバージョンに設定し、ビルド をアプリのビルド番号 (pubspec.yamlと同じ値) に設定します。
$(FLUTTER_BUILD_NAME) / $(FLUTTER_BUILD_NUMBER) をウィジェットの Info.plist に貼り付けてこれらを設定しようとしないでください。これらの変数は、Runner ターゲットに対してのみ定義されます (Flutter の Generated.xcconfig 経由)。ウィジェット ターゲットでは空に解決されるため、バージョンは暗黙的に一致しません。上記のように、ターゲットのビルド設定 (MARKETING_VERSION / CURRENT_PROJECT_VERSION) を使用します。これは送信要件のみであり、不一致によっても実行時にアプリがクラッシュすることはありません。
- Flutter の依存関係をリンクしないでください:
FlutterGeneratedPluginSwiftPackageまたは Flutter エンジンをウィジェット拡張機能にリンクしないでください。これを行うと、Flutter は動的 SPM フレームワークを App Extensions に埋め込まないため、リリース モードでは起動時にdyldクラッシュ (Library not loaded) が発生します。 - 自動生成された
TraceletWidgetLiveActivity.swiftコンテンツを次のものに置き換えます。ここでは、SDK をインポートする代わりに、TraceletActivityAttributes構造体を手動で定義していることに注意してください。ActivityKit は、構造体の (修飾されていない) 名前と形状によってアクティビティを照合します。そのため、これによりウィジェットが軽量に保たれ、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.title と context.state.status からのデータをマッピングします。
ウィジェット拡張機能の展開ターゲットを iOS 16.2 に設定します。 Xcode の「ウィジェット拡張機能」テンプレートは、多くの場合、コントロール ウィジェットと @main WidgetBundle に @available(iOS 18.0, *) アノテーションを追加します。そのアノテーションが拡張機能のデプロイ ターゲットよりも高い場合、ウィジェット バンドル (およびライブ アクティビティ) は通知なく登録に失敗し、システム ログに Activity had no descriptor が記録され、古い OS バージョンでは拡張機能がクラッシュする可能性があります。 @main バンドルを拡張機能の導入フロアで利用できるようにし、iOS 18 専用のウィジェット (コントロール ウィジェットなど) を if #available(iOS 18.0, *) でラップします。
シナリオ 6: 終了状態 (アプリの強制終了)
検討した概念: アプリのライフサイクル、強制終了と OS の一時停止
問題
ユーザーがアプリ スイッチャーから上にスワイプし、アプリを完全に強制終了します。 Tracelet がバックグラウンドで追跡を続けることを期待しますが、位置情報の受信は停止します。
Apple の対応方法
フォアグラウンド サービスがスワイプして閉じても生き残ることができる Android とは異なり、iOS はユーザーの意図を厳密に強制します。
Apple の公式 CoreLocation ドキュメント によると:
「ユーザーがアプリを強制終了した場合、新しい位置イベントが到着したときにシステムは自動的にアプリを起動しません。システムが位置イベントの配信を再開する前に、ユーザーは明示的にアプリを再起動する必要があります。」
ユーザーがアプリを手動で強制終了すると:
- 標準の位置情報更新: 完全に停止しました。
- 場所の大幅な変更 (SLC): 完全に停止しました。
- 地域監視 (ジオフェンス): 完全に停止しました。
終了状態でも動作しますか?
- OS がアプリを終了した場合 (バックグラウンドでのメモリ不足などにより): はい。 Apple は、位置情報イベント (重大な変更やジオフェンスのトリガーなど) が発生すると、バックグラウンドでアプリを自動的に再起動します。 Tracelet のネイティブ Rust コアが起動し、シームレスに処理します。
- ユーザーがアプリを明示的に強制終了した場合: いいえ。ユーザーが手動でアプリ アイコンをタップして再度開くまで、アプリは停止したままになります。
Tracelet がそれを解決する方法
Tracelet は Apple の基本的な OS 制約を回避できません。ただし、Tracelet は次のことを保証します。
- すべての非同期の場所は、アプリが強制終了される前に、Rust SQLite データベースに安全に保存されます。
- ユーザーがアプリを再度開くとすぐに、Tracelet はすぐに追跡を再開し、オフライン データを隙なく同期します。