axiom-background-processing について
axiom-background-processingは、iOS開発者向けのバックグラウンド処理の実装・診断スキルです。登録、スケジュール制約、タスクの種類、期限切れ時の処理を解説し、タスクが実行・完了しない原因を調べます。
主な機能
- 01タスク識別子と登録タイミングの確認
- 02iOSバックグラウンドタスクの種類の選択
- 03期限切れへの対応と進捗の保存
- 04Consoleログによるスケジュール診断
- 05LLDBでのテストとシミュレーター制約の確認
活用シーン
- 01タスクが開始されない問題の診断
- 02コンテンツ更新や保守に使うタスクの選択
- 03システムによる終了時に途中の進捗を保持
- 04開発環境と本番環境の差の調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-background-processingインストール後、Claude Code で/axiom-background-processingを入力して読み込まれたか確認します。
詳細
バックグラウンド処理
概要
バックグラウンド実行は権利ではなく、特権です。iOS はバッテリー駆動時間とユーザー体験を守るため、バックグラウンド処理を積極的に制限します。基本原則:バックグラウンドタスクは任意のジョブとして扱ってください。実行時間枠をリクエストし、コードを実行するタイミング(または実行するかどうか)はシステムが決定します。
重要なポイント:「タスクが一度も実行されない」という問題の多くは、登録ミスか、実行を左右する7つのスケジューリング要因についての誤解が原因です。このスキルでは、当てずっぽうではなく、体系的なデバッグ方法を紹介します。
エネルギーの最適化:バックグラウンドタスクによるバッテリーへの影響を減らす方法については、axiom-energy スキルを参照してください。このスキルでは、タスクを正しく実行し、確実に完了させるための仕組みに焦点を当てます。
要件:iOS 13 以降(BGTaskScheduler)、iOS 26 以降(BGContinuedProcessingTask)、Xcode 15 以降
質問例
このスキルで回答する、開発者からよく寄せられる質問:
1. 「バックグラウンドタスクが一度も実行されません。登録して、スケジュールも設定しましたが、何も起こりません。」
→ このスキルでは、「タスクが一度も実行されない」問題に対する登録チェックリストとデバッグの判断ツリーを説明します。
2. 「バックグラウンドタスクをテストするにはどうすればよいですか?シミュレーターで実行されないようです。」
→ このスキルでは、LLDB のデバッグコマンドとシミュレーターの制限について説明します。
3. 「タスクが完了前に終了させられます。実行時間を延ばすにはどうすればよいですか?」
→ このスキルでは、タスクの種類(BGAppRefresh は30秒、BGProcessing は数分)、有効期限ハンドラー、進捗を段階的に保存する方法について説明します。
4. 「BGAppRefreshTask と BGProcessingTask のどちらを使うべきですか?違いは何ですか?」
→ このスキルでは、作業時間とシステム要件に基づいて適切なタスクの種類を選ぶための判断ツリーを紹介します。
5. 「Swift 6 の並行処理をバックグラウンドタスクの有効期限と統合するにはどうすればよいですか?」
→ このスキルでは、BGTask の有効期限と構造化並行処理を連携させるための withTaskCancellationHandler パターンについて説明します。
6. 「バックグラウンドタスクが開発環境では動作しますが、本番環境では動作しません。」
→ このスキルでは、7つのスケジューリング要因、スロットリングの動作、本番環境でのデバッグについて説明します。
注意すべき兆候 — タスクが実行されない、または終了させられる
次のいずれかに該当する場合は、登録またはスケジューリングの問題を疑ってください。
- タスクが一度も実行されない:
submit()が成功したにもかかわらず、ハンドラーが呼び出されない - タスクがすぐに終了する:ハンドラーは呼び出されるが、作業が完了しない
- 開発環境では動作するが、本番環境では動作しない:デバッガー接続中はタスクが実行されるが、リリースビルドでは実行されない
- コンソールに起動ログが表示されない:統合ログに「BackgroundTask」の項目がない
- 識別子の不一致エラー:タスク識別子が Info.plist と一致しない
- 「ハンドラーが登録されていません」:最初のスケジューリングより前にハンドラーが登録されていない
エネルギー関連の問題との違い
- エネルギー関連の問題:タスクは実行されるが、バッテリーを消耗する(
axiom-energyスキルを参照) - このスキル:タスクが実行されない、または作業完了前に終了する
必ず最初に行う手順
コードをデバッグする前に、必ず次の項目を確認してください:
手順 1:Info.plist の設定を確認する(2分)
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.yourapp.refresh</string>
<string>com.yourapp.processing</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>
<array>
<string>fetch</string>
<string>processing</string>
</array>
よくある間違い:コード内の識別子が Info.plist と完全には一致していません。誤字や大文字と小文字の違いがないか確認してください。
手順 2:登録のタイミングを確認する(2分)
アプリの起動が完了する前に、必ず登録してください。
// ✅ CORRECT: Register in didFinishLaunchingWithOptions
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.refresh",
using: nil
) { task in
// Safe force cast: identifier guarantees BGAppRefreshTask type
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
return true // Register BEFORE returning
}
// ❌ WRONG: Registering after launch or on-demand
func someButtonTapped() {
// TOO LATE - registration won't work
BGTaskScheduler.shared.register(...)
}
例外:BGContinuedProcessingTask(iOS 26 以降)では、ユーザーが操作を開始したときに動的に登録します。
手順 3:コンソールログを確認する(5分)
バックグラウンドタスクのイベントで Console.app を絞り込みます。
subsystem:com.apple.backgroundtaskscheduler
次の項目を探します。
- 「識別子を指定してタスクのハンドラーを登録しました」
- 「識別子を指定してタスクをスケジュールしています」
- 「識別子を指定してタスクを開始しています」
- 「識別子を指定してタスクが完了しました」
- ハンドラーや識別子の不足に関するエラーメッセージ
手順 4:アプリがスワイプで終了されていないことを確認する(1分)
重要:ユーザーがアプリスイッチャーからアプリを強制終了した場合、バックグラウンドタスクは一切実行されません。
アプリスイッチャーで、アプリがまだ表示されているか確認してください。スワイプして終了すると、ユーザーが再びアプリを起動するまでバックグラウンドで実行されません。
バックグラウンドタスクの判断ツリー
Need to run code in the background?
│
├─ User initiated the action explicitly (button tap)?
│ ├─ iOS 26+? → BGContinuedProcessingTask (Pattern 4)
│ └─ iOS 13-25? → beginBackgroundTask + save progress (Pattern 5)
│
├─ Keep content fresh throughout the day?
│ ├─ Runtime needed ≤ 30 seconds? → BGAppRefreshTask (Pattern 1)
│ └─ Need several minutes? → BGProcessingTask with constraints (Pattern 2)
│
├─ Deferrable maintenance work (DB cleanup, ML training)?
│ └─ BGProcessingTask with requiresExternalPower (Pattern 2)
│
├─ Large downloads/uploads?
│ └─ Background URLSession (Pattern 6)
│
├─ Triggered by server data changes?
│ └─ Silent push notification → fetch data → complete handler (Pattern 7)
│
└─ Short critical work when app backgrounds?
└─ beginBackgroundTask (Pattern 5)
タスクタイプの比較
| タイプ | 実行時間 | 実行タイミング | 用途 |
|---|---|---|---|
| BGAppRefreshTask | 約30秒 | ユーザーのアプリ使用パターンに基づく | 最新コンテンツの取得 |
| BGProcessingTask | 数分 | デバイスの充電中、アイドル時(通常は夜間) | メンテナンス、ML トレーニング |
| BGContinuedProcessingTask | 長時間 | 進捗 UI を使用してシステムが管理 | ユーザーが開始するエクスポート/公開 |
| beginBackgroundTask | 約30秒 | アプリがバックグラウンドに移行するとすぐ | 状態の保存、アップロードの完了 |
| Background URLSession | 必要に応じて | アプリ終了後も含め、システムに適したタイミング | 大容量データの転送 |
よくあるパターン
パターン 1:BGAppRefreshTask — コンテンツを最新に保つ
使用する場面:ユーザーがアプリを開いたときに最新の状態に見えるよう、新しいコンテンツを取得する必要がある場合です。
実行時間:約30秒
システムの実行タイミング:ユーザーのアプリ使用パターンに基づいて予測されます。ユーザーが毎朝アプリを開く場合、システムはそのパターンを学習し、その前に更新します。
登録(アプリ起動時)
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.refresh",
using: nil
) { task in
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
return true
}
スケジュール設定(アプリがバックグラウンドに移行するとき)
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.yourapp.refresh")
// earliestBeginDate = MINIMUM delay, not exact time
// System may run hours later based on usage patterns
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // At least 15 min
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Failed to schedule refresh: \(error)")
}
}
// Call when app enters background
func applicationDidEnterBackground(_ application: UIApplication) {
scheduleAppRefresh()
}
// Or with SceneDelegate / SwiftUI
.onChange(of: scenePhase) { newPhase in
if newPhase == .background {
scheduleAppRefresh()
}
}
ハンドラー
func handleAppRefresh(task: BGAppRefreshTask) {
// 1. IMMEDIATELY set expiration handler
task.expirationHandler = { [weak self] in
// Cancel any in-progress work
self?.currentOperation?.cancel()
}
// 2. Schedule NEXT refresh (continuous refresh pattern)
scheduleAppRefresh()
// 3. Do the work
fetchLatestContent { [weak self] result in
switch result {
case .success:
task.setTaskCompleted(success: true)
case .failure:
task.setTaskCompleted(success: false)
}
}
}
重要なポイント:
- 最初に有効期限ハンドラーを設定します
- ハンドラー内で次の更新をスケジュールします(継続的なパターン)
- すべてのコードパス(成功時と失敗時の両方)で
setTaskCompletedを呼び出します - 作業を30秒以内に収めます
パターン 2:BGProcessingTask — 延期可能なメンテナンス
使用する場面:システムの条件が最適になる(充電中、Wi-Fi 接続中、アイドル時)まで待てるメンテナンス作業です。
実行時間:数分
システムの実行タイミング:通常、デバイスの充電中に夜間実行されます。毎日実行されるとは限りません。
登録
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.maintenance",
using: nil
) { task in
self.handleMaintenance(task: task as! BGProcessingTask)
}
制約付きのスケジュール設定
func scheduleMaintenanceIfNeeded() {
// Be conscientious — only schedule when work is actually needed
guard needsMaintenance() else { return }
let request = BGProcessingTaskRequest(identifier: "com.yourapp.maintenance")
// CRITICAL: Set requiresExternalPower for CPU-intensive work
request.requiresExternalPower = true
// Optional: Require network for cloud sync
request.requiresNetworkConnectivity = true
// Don't set earliestBeginDate too far — max ~1 week
// If user doesn't return to app, task won't run
do {
try BGTaskScheduler.shared.submit(request)
} catch BGTaskScheduler.Error.unavailable {
print("Background processing not available")
} catch {
print("Failed to schedule: \(error)")
}
}
進捗チェックポイントを使用するハンドラー
func handleMaintenance(task: BGProcessingTask) {
var shouldContinue = true
task.expirationHandler = { [weak self] in
shouldContinue = false
self?.saveProgress() // Save partial progress!
}
Task {
do {
// Process in chunks, checking for expiration
for chunk in workChunks {
guard shouldContinue else {
// Expiration called — stop gracefully
break
}
try await processChunk(chunk)
saveProgress() // Checkpoint after each chunk
}
task.setTaskCompleted(success: true)
} catch {
task.setTaskCompleted(success: false)
}
}
}
重要なポイント:
- CPU 負荷の高い作業では
requiresExternalPower = trueを設定します(バッテリー消費を防止します) - 進捗を段階的に保存します。タスクが中断される場合があります
- ユーザーがデバイスを充電しない場合、作業が一度も実行されない可能性があります
earliestBeginDateを1週間より先に設定しないでください
パターン 3:SwiftUI の backgroundTask モディファイア
使用する場面:最新の async/await パターンを使用する SwiftUI アプリです。
@main
struct MyApp: App {
@Environment(\.scenePhase) var scenePhase
var body: some Scene {
WindowGroup {
ContentView()
}
.onChange(of: scenePhase) { newPhase in
if newPhase == .background {
scheduleAppRefresh()
}
}
// Handle app refresh
.backgroundTask(.appRefresh("com.yourapp.refresh")) {
// Schedule next refresh
scheduleAppRefresh()
// Async work — task completes when closure returns
await fetchLatestContent()
}
// Handle background URLSession events
.backgroundTask(.urlSession("com.yourapp.downloads")) {
// Called when background URLSession completes
await processDownloadedFiles()
}
}
}
SwiftUI の利点:
- クロージャーが戻るとタスクが暗黙的に完了します(
setTaskCompletedは不要です) - Swift Concurrency をネイティブでサポートします
- 有効期限が切れるとタスクは自動的にキャンセルされます
パターン 4:BGContinuedProcessingTask(iOS 26 以降)
使用する場面:ユーザーが明示的に開始(ボタンをタップ)した作業を、進捗を表示しながらバックグラウンド移行後も継続する場合です。
使用しない場面:自動タスク、メンテナンス、同期
// 1. Info.plist — use wildcard for dynamic suffix
// BGTaskSchedulerPermittedIdentifiers:
// "com.yourapp.export.*"
// 2. Register WHEN user initiates action (not at launch)
func userTappedExportButton() {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.export.photos"
) { task in
let continuedTask = task as! BGContinuedProcessingTask
self.handleExport(task: continuedTask)
}
// Submit immediately
let request = BGContinuedProcessingTaskRequest(
identifier: "com.yourapp.export.photos",
title: "Exporting Photos",
subtitle: "0 of 100 photos"
)
// Optional: Fail if can't start immediately
request.strategy = .fail // or .enqueue (default)
do {
try BGTaskScheduler.shared.submit(request)
} catch {
showError("Cannot export in background right now")
}
}
// 3. Handler with mandatory progress reporting
func handleExport(task: BGContinuedProcessingTask) {
var shouldContinue = true
task.expirationHandler = {
shouldContinue = false
}
// MANDATORY: Report progress (tasks with no progress auto-expire)
task.progress.totalUnitCount = 100
task.progress.completedUnitCount = 0
Task {
for (index, photo) in photos.enumerated() {
guard shouldContinue else { break }
await exportPhoto(photo)
// Update progress — system shows this to user
task.progress.completedUnitCount = Int64(index + 1)
}
task.setTaskCompleted(success: shouldContinue)
}
}
重要なポイント:
- 動的に登録します(起動時ではなく、ユーザーの操作時)
- 進捗の報告は必須です。更新のないタスクは自動的に期限切れになります
- ユーザーはシステム UI から進捗を確認し、キャンセルできます
- 作業をすぐに開始できる場合にのみ有用であれば、
.fail戦略を使用します
パターン 5:beginBackgroundTask — 短時間の重要な作業
使用する場面:アプリがバックグラウンドに移行するとき、重要な作業(状態の保存、アップロードの完了)を終えるために約30秒必要な場合です。
var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
func applicationDidEnterBackground(_ application: UIApplication) {
// Start background task
backgroundTaskID = application.beginBackgroundTask(withName: "Save State") { [weak self] in
// Expiration handler — clean up and end task
self?.saveProgress()
if let taskID = self?.backgroundTaskID {
application.endBackgroundTask(taskID)
}
self?.backgroundTaskID = .invalid
}
// Do critical work
saveEssentialState { [weak self] in
// End task as soon as done — DON'T wait for expiration
if let taskID = self?.backgroundTaskID, taskID != .invalid {
UIApplication.shared.endBackgroundTask(taskID)
self?.backgroundTaskID = .invalid
}
}
}
重要なポイント:
- 作業が完了したら、できるだけ早く
endBackgroundTaskを呼び出します(有効期限ハンドラー内で呼び出すだけでは不十分です) - タスクを終了しないと、システムがアプリを終了させ、今後の起動に影響する可能性があります
- 最大で約30秒ですが、保証されるものではありません
- 継続的な作業ではなく、状態の保存に使用します
パターン 6:Background URLSession
使用する場面:アプリが終了しても継続する必要がある、大容量のダウンロード/アップロードです。
// 1. Create background configuration
lazy var backgroundSession: URLSession = {
let config = URLSessionConfiguration.background(
withIdentifier: "com.yourapp.downloads"
)
config.sessionSendsLaunchEvents = true // App relaunched when complete
config.isDiscretionary = true // System chooses optimal time
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
// 2. Start download
func downloadFile(from url: URL) {
let task = backgroundSession.downloadTask(with: url)
task.resume()
}
// 3. Handle app relaunch for session events (AppDelegate)
func application(_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void) {
// Store completion handler — call after processing events
backgroundSessionCompletionHandler = completionHandler
// Session delegate methods will be called
}
// 4. URLSessionDelegate
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
// All events processed — call stored completion handler
DispatchQueue.main.async {
self.backgroundSessionCompletionHandler?()
self.backgroundSessionCompletionHandler = nil
}
}
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
// Move file from temp location before returning
let destinationURL = getDestinationURL(for: downloadTask)
try? FileManager.default.moveItem(at: location, to: destinationURL)
}
重要なポイント:
- 作業はシステムデーモン(
nsurlsessiond)に引き渡され、アプリの終了後も継続されます - 緊急でない場合は
isDiscretionary = trueを設定します(システムは Wi-Fi 接続中、充電中になるまで待機します) - アプリの再起動に備えて
handleEventsForBackgroundURLSessionを処理する必要があります - ダウンロードしたファイルはすぐに移動します。デリゲートから戻った後、一時ファイルの保存場所は削除されます
パターン 7:サイレントプッシュ通知による起動
使用する場面:サーバーからアプリを起動して、新しいデータを取得する必要がある場合です。
サーバーペイロード
{
"aps": {
"content-available": 1
},
"custom-data": "fetch-new-messages"
}
エネルギー効率を高めるため、apns-priority: 10 ではなく apns-priority: 5 を使用します。
App Handler
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
Task {
do {
let hasNewData = try await fetchLatestData()
completionHandler(hasNewData ? .newData : .noData)
} catch {
completionHandler(.failed)
}
}
}
重要なポイント:
- サイレントプッシュにはレート制限があります — すべてのプッシュで起動することを期待しないでください
- システムは複数のプッシュを合算します(14 回のプッシュが 7 回の起動になる場合があります)
- バジェットは起動ごとに消費され、1 日のうちに補充されます
- 1 回の起動あたり約 30 秒の実行時間
サイレントプッシュ通知のパターン(content-available ペイロード、スロットリング制限、APNs の設定)については、axiom-push-notifications を参照してください。
Swift 6 のキャンセル統合
構造化並行性を使用する場合、BGTask の期限切れをタスクのキャンセルにブリッジします:
func handleAppRefresh(task: BGAppRefreshTask) {
// Create a Task that respects expiration
let workTask = Task {
try await withTaskCancellationHandler {
// Your async work
try await fetchAndProcessData()
task.setTaskCompleted(success: true)
} onCancel: {
// Called synchronously when task.cancel() is invoked
// Note: Runs on arbitrary thread, keep lightweight
}
}
// Bridge expiration to cancellation
task.expirationHandler = {
workTask.cancel() // Triggers onCancel block
}
}
// Checking cancellation in your work
func fetchAndProcessData() async throws {
for item in items {
// Check if we should stop
try Task.checkCancellation()
// Or non-throwing check
guard !Task.isCancelled else {
saveProgress()
return
}
try await process(item)
}
}
重要なポイント:
withTaskCancellationHandlerはタスクが中断されている間のキャンセルを処理しますTask.checkCancellation()はキャンセルされている場合にCancellationErrorをスローします- スローしないチェックには
Task.isCancelledを使用します - キャンセルは協調的です — コード側で確認して対応する必要があります
バックグラウンドタスクのテスト
シミュレータの制限
バックグラウンドタスクはシミュレータでは自動的に実行されません。手動でトリガーする必要があります。
LLDB デバッグコマンド
デバッガをアタッチした状態でアプリを実行している間に、実行を一時停止して次を実行します:
// Trigger task launch
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.yourapp.refresh"]
// Trigger task expiration (test expiration handler)
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.yourapp.refresh"]
テストのワークフロー
- タスクハンドラにブレークポイントを設定する
- アプリを実行し、バックグラウンドに移行させる
- デバッガで一時停止する
_simulateLaunchForTaskWithIdentifierコマンドを実行する- 再開する — ブレークポイントにヒットするはずです
_simulateExpirationForTaskWithIdentifierで期限切れをテストする
テストチェックリスト
- シミュレート時にタスクハンドラのブレークポイントにヒットしますか?
- シミュレート時に期限切れハンドラが呼び出されますか?
- すべてのコードパスで
setTaskCompletedが呼び出されていますか? - 実機(シミュレータだけでなく)で動作しますか?
- リリースビルド(デバッグだけでなく)で動作しますか?
- アプリが App Switcher からスワイプして消されていませんか?
7 つのスケジューリング要因
WWDC 2020-10063「Background execution demystified」より:
| 要因 | 説明 | 影響 |
|---|---|---|
| バッテリー残量が危機的に少ない | バッテリー残量 <20% | すべての裁量的な作業が一時停止 |
| 低電力モード | ユーザーが有効化 | バックグラウンドアクティビティが制限される |
| アプリの使用状況 | ユーザーがアプリを起動する頻度 | 使用が多い = 優先度が高い |
| App Switcher | アプリがまだ表示されているか? | スワイプして消されている = バックグラウンドなし |
| バックグラウンド App Refresh | システム設定 | オフ = BGAppRefresh タスクなし |
| システムバジェット | エネルギー/データのバジェット | 起動ごとに消費され、1 日のうちに補充される |
| レート制限 | システムによる間隔調整 | 頻繁すぎる起動を防ぐ |
システムの制約への対応
// Check Low Power Mode
if ProcessInfo.processInfo.isLowPowerModeEnabled {
// Reduce background work
}
// Listen for changes
NotificationCenter.default.publisher(for: .NSProcessInfoPowerStateDidChange)
.sink { _ in
// Adapt behavior
}
// Check Background App Refresh status
let status = UIApplication.shared.backgroundRefreshStatus
switch status {
case .available:
break // Good to schedule
case .denied:
// User disabled — prompt to enable in Settings
case .restricted:
// Parental controls or MDM — can't enable
}
監査チェックリスト
登録チェックリスト
- Info.plist の識別子がコードと完全に一致していますか(大文字と小文字を区別)?
- 正しいバックグラウンドモード(
fetch、processing)が有効になっていますか? - 登録は
didFinishLaunchingWithOptions内で return の前に実行されていますか? - 同じ識別子を複数回登録していませんか?
- ハンドラクロージャが self を強参照でキャプチャしていませんか?
スケジューリングチェックリスト
- メインキューまたはバックグラウンドキュー(パフォーマンスが重要な場合)でスケジューリングしていますか?
-
earliestBeginDateが遠すぎませんか(最大約 1 週間)? -
submit()のエラーを処理していますか? - 重複したタスクをスケジューリングしていませんか(
getPendingTaskRequestsを確認)?
ハンドラチェックリスト
- 期限切れハンドラをハンドラの開始時にすぐに設定していますか?
- すべてのコードパスで
setTaskCompleted(success:)が呼び出されていますか? - 次のタスクがスケジューリングされていますか(継続的なパターンの場合)?
- 長時間の操作では進捗が段階的に保存されていますか?
- 期限切れハンドラが進行中の作業を実際にキャンセルしていますか?
本番環境への準備
- シミュレータだけでなく実機でテストしましたか?
- デバッグだけでなくリリースビルドでテストしましたか?
- 低電力モードを有効にしてテストしましたか?
- App Switcher からの強制終了後にテストしましたか(実行されるべきではありません)?
- コンソールログに期待どおりの「Task completed」メッセージが表示されますか?
プレッシャーシナリオ
シナリオ 1:「バックグラウンドで 30 秒ごとにサーバーをポーリングするだけ」
誘惑:「プッシュ通知よりポーリングのほうが簡単です。リアルタイムの更新が必要です。」
実際のところ:
- iOS では 30 秒間隔のバックグラウンド実行はできません
- BGAppRefreshTask は、設定したスケジュールではなく、ユーザーの行動パターンに基づいて実行されます
- ユーザーがアプリをほとんど開かない場合、タスクの実行は 1 日に 1 回以下になることがあります
- ポーリングは実行予算をすぐに使い切るため、起動回数が減ります
所要時間の比較:
- ポーリングの実装:30 分(想定どおりには動作しません)
- 動作しない理由の把握:デバッグに 2~4 時間
- 適切なプッシュ通知の実装:3~4 時間
実際に有効な方法:
- サイレントプッシュ通知(ポーリングではなく、サーバーがトリガーします)
- ユーザーの行動予測に基づく BGAppRefreshTask(リアルタイムではありません)
- 後回しにできる作業向けの BGProcessingTask(夜間)
押し返すときの例文:「iOS のバックグラウンド実行では、ポーリング間隔を指定できません。BGAppRefreshTask は、固定スケジュールではなく、iOS がユーザーがアプリを開くと予測するタイミングに基づいて実行されます。リアルタイムの更新には、サーバー側からのプッシュ通知が必要です。Apple のドキュメントをお見せします。」
シナリオ 2:「このタスクには 30 秒ではなく 5 分必要です」
誘惑:「beginBackgroundTask を使って、すべての作業を実行すればいいだけです。」
実際のところ:
- beginBackgroundTask:最大約 30 秒
- BGAppRefreshTask:約 30 秒
- BGProcessingTask:数分間ですが、充電中に限られます
- フォアグラウンドと同等の実行時間を 5 分間保証する API はありません
実際に有効な方法:
- 作業を分割する — 30 秒で完了する単位に分け、進捗を保存します
- BGProcessingTask を使用する(
requiresExternalPower = true)(夜間に実行されます) - iOS 26 以降:ユーザーが開始した作業には BGContinuedProcessingTask を使用します
押し返すときの例文:「iOS はバッテリーを保護するため、バックグラウンドの実行時間を制限しています。数分かかる作業には、2 つの方法があります。(1)充電中に夜間実行する BGProcessingTask を使う方法。メンテナンスに適しています。(2)作業を 30 秒以内に完了する単位に分け、実行ごとに進捗を保存する方法です。どちらが今回の用途に合っていますか?」
シナリオ 3:「自分のデバイスでは動くのに、ユーザーには動きません」
誘惑:「コードは正しいので、ユーザーのデバイスに問題があるはずです。」
実際のところ:Xcode を接続したデバッグビルドの動作は、実際の環境にあるリリースビルドとは異なります。
よくある原因:
- 低電力モードが有効 — バックグラウンドでの動作が制限されます
- App のバックグラウンド更新が無効 — ユーザーまたはペアレンタルコントロールによって無効化されています
- アプリをスワイプして終了した — すべてのバックグラウンドタスクが終了します
- 実行予算を使い切った — 最近の起動回数が多すぎます
- あまり使われていないアプリ — システムによって優先度が下げられます
デバッグ手順:
- 起動時に
backgroundRefreshStatusを確認し、ログに記録します - タスクのスケジュール時と完了時にログを記録します
- MetricKit を使って、本番環境でのバックグラウンド起動を監視します
- ユーザーに「App スイッチャーからアプリを強制終了しましたか?」と尋ねます
押し返すときの例文:「バックグラウンド実行は、バッテリー残量、ユーザーのアプリ利用パターン、アプリを強制終了したかどうかなど、7 つのシステム要因に左右されます。影響を受けているユーザーの状況を把握できるよう、ログを追加します。」
シナリオ 4:「今すぐリリースして、バックグラウンドタスクは後で追加する」
誘惑:「バックグラウンド処理は、あれば便利な機能です。」
実際のところ:
- ユーザーはアプリを開いたときに、コンテンツが最新であることを期待します
- バックグラウンドで更新する競合アプリのほうが、応答性が高く感じられます
- 後からバックグラウンドタスクを追加するには、登録タイミングを慎重に扱う必要があります
- 古いコンテンツを最初に見せると、ユーザーの継続利用に影響します
所要時間の比較:
- BGAppRefreshTask を今追加する:1~2 時間
- 適切なテストを含めて後から追加する:4~6 時間
- 「なぜ動かないのか」という問題のデバッグ:さらに数時間
最低限のバックグラウンド処理:
// In didFinishLaunchingWithOptions
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.refresh",
using: nil
) { task in
task.setTaskCompleted(success: true) // Placeholder
self.scheduleRefresh()
}
押し返すときの例文:「バックグラウンド更新は、[アプリの種類]では基本的に期待される機能です。最低限の実装は 20 行のコードで済みます。実装せずにリリースして後から追加すると、登録タイミングのバグが発生するリスクがあります。リリース後に機能を強化できるよう、今のうちに基本部分を追加します。」
実例
例 1:タスクが実行されない — 識別子の不一致
症状:submit() は成功しますが、ハンドラーが呼び出されません。
診断:
// Code uses:
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.myapp.Refresh", // Capital R
...
)
// Info.plist has:
// "com.myapp.refresh" // lowercase r
修正:識別子は大文字と小文字を区別するため、完全に一致させる必要があります。
無駄になった時間:問題はタイプミスだったのに、コードのロジックのデバッグに 2 時間を費やしました。
例 2:タスクが終了する — setTaskCompleted の呼び出し漏れ
症状:ハンドラーが実行され、処理が完了したように見えますが、次にスケジュールされたタスクが実行されません。
診断:
func handleRefresh(task: BGAppRefreshTask) {
fetchData { result in
switch result {
case .success:
task.setTaskCompleted(success: true) // ✅ Called
case .failure:
// ❌ Missing setTaskCompleted!
print("Failed")
}
}
}
修正:エラー時を含め、すべてのコードパスで setTaskCompleted を呼び出します。
case .failure:
task.setTaskCompleted(success: false) // ✅ Now called
影響:setTaskCompleted を呼び出さないと、システムによってアプリのバックグラウンド実行枠が減らされる可能性があります。
例 3:開発環境では動作するが、本番環境では動作しない — 強制終了
症状:ユーザーからバックグラウンド同期が動作しないと報告されます。開発者は問題を再現できません。
診断:
User: "I close my apps every night to save battery."
Developer: "How do you close them?"
User: "Swipe up in the app switcher."
実情:App Switcher からアプリをスワイプして閉じると、アプリが強制終了され、ユーザーが再びアプリを開くまでバックグラウンドタスクは実行されません。
修正:
- ユーザーに説明する(理想的ではありません)
- これが iOS の動作であることを受け入れる
- アプリの再起動時に、初回起動時の操作性を確保する
例 4:BGProcessingTask が実行されない — 電源要件の不足
症状:BGProcessingTask がスケジュールされても実行されません。
診断:ユーザーは夜間にスマートフォンを充電していますが、タスクの requiresExternalPower = true が設定されており、ユーザーはワイヤレス充電器を使用しています。
いいえ、それが問題ではありません。本当の問題は次のとおりです。
let request = BGProcessingTaskRequest(identifier: "com.app.maintenance")
// Missing: request.requiresExternalPower = true
requiresExternalPower が設定されていなくても、システムは充電されるまで待機しますが、確実性は低くなります。明示的に設定すると、システムに明確なシグナルを送れます。
また、処理タスクの対象となるには、ユーザーが約 2 週間以内にアプリをフォアグラウンドで起動している必要があります。
クイックリファレンス
LLDB デバッグコマンド
// Trigger task
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"IDENTIFIER"]
// Trigger expiration
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"IDENTIFIER"]
コンソールフィルター
subsystem:com.apple.backgroundtaskscheduler
タスクタイプの概要
| 必要な処理 | 使用するもの | 実行時間 |
|---|---|---|
| コンテンツを最新の状態に保つ | BGAppRefreshTask | 約30秒 |
| 負荷の高いメンテナンス | BGProcessingTask + requiresExternalPower | 数分 |
| ユーザーが開始した処理の継続 | BGContinuedProcessingTask (iOS 26) | 長時間 |
| バックグラウンド移行時に処理を完了する | beginBackgroundTask | 約30秒 |
| 大容量のダウンロード | Background URLSession | 必要に応じて |
| サーバーによるトリガー | サイレントプッシュ通知 | 約30秒 |
リソース
WWDC:2019-707、2020-10063、2022-10142、2023-10170、2025-227
ドキュメント:/backgroundtasks/bgtaskscheduler、/backgroundtasks/starting-and-terminating-tasks-during-development
スキル:axiom-background-processing-ref、axiom-background-processing-diag、axiom-energy、axiom-push-notifications
最終更新日:2025-12-31 プラットフォーム:iOS 13 以降、iOS 26 以降(BGContinuedProcessingTask) ステータス:本番環境で利用可能なバックグラウンドタスクのパターン