background-processing について
iOS 開発者向けに、BackgroundTasks、バックグラウンド URLSession、プッシュ通知を使った処理を解説するスキルです。設定やスケジュール、キャンセル処理、実装上の注意点を扱います。
主な機能
- 01BGTaskScheduler と識別子の設定
- 02更新タスクと処理タスクのスケジュール
- 03フォアグラウンド開始の継続タスク
- 04バックグラウンド URLSession によるダウンロード
- 05バックグラウンドプッシュによる更新の起動
- 06期限切れ、キャンセル、完了の処理
活用シーン
- 01アプリデータの定期更新
- 02デバイスのアイドル時のメンテナンス
- 03アプリ停止後も続けるダウンロード
- 04サイレントプッシュ後のコンテンツ取得
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill background-processingインストール後、Claude Code で/background-processingを入力して読み込まれたか確認します。
同梱ファイルevals/evals.jsonreferences/background-task-patterns.md
詳細
バックグラウンド処理
BackgroundTasks フレームワーク、バックグラウンド URLSession、バックグラウンドプッシュ通知を使用して、iOS でバックグラウンド作業を登録、スケジュール、実行します。
目次
- Info.plist の設定
- BGTaskScheduler の登録
- BGAppRefreshTask のパターン
- BGProcessingTask のパターン
- BGContinuedProcessingTask(iOS 26 以降)
- バックグラウンド URLSession ダウンロード
- バックグラウンドプッシュのトリガー
- よくある間違い
- レビュー用チェックリスト
- 参考資料
Info.plist の設定
すべてのタスク識別子は、Info.plist の BGTaskSchedulerPermittedIdentifiers に宣言する必要があります。宣言されていない場合、submit(_:) は BGTaskScheduler.Error.Code.notPermitted をスローします。
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.example.app.refresh</string>
<string>com.example.app.db-cleanup</string>
<string>com.example.app.export.*</string>
</array>
必要な UIBackgroundModes も有効にします。
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>processing</string>
</array>
Xcode で、ターゲット > Signing & Capabilities > Background Modes を選び、「Background fetch」と「Background processing」を有効にします。
BGTaskScheduler の登録
アプリの起動が完了する前にハンドラーを登録します。UIKit では application(_:didFinishLaunchingWithOptions:) で、SwiftUI では App.init() で登録します。
UIKit での登録
import BackgroundTasks
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.example.app.refresh",
using: nil // nil = default background queue
) { task in
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.example.app.db-cleanup",
using: nil
) { task in
self.handleDatabaseCleanup(task: task as! BGProcessingTask)
}
return true
}
}
SwiftUI での登録
import SwiftUI
import BackgroundTasks
@main
struct MyApp: App {
init() {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.example.app.refresh",
using: nil
) { task in
BackgroundTaskManager.shared.handleAppRefresh(
task: task as! BGAppRefreshTask
)
}
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
BGAppRefreshTask のパターン
小規模なデータ更新を取得するための、短時間(約 30 秒)のタスクです。起動するタイミングはシステムが決定します。earliestBeginDate は下限のヒントにすぎません。
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(
identifier: "com.example.app.refresh"
)
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule app refresh: \(error)")
}
}
func handleAppRefresh(task: BGAppRefreshTask) {
// Schedule the next refresh before doing work
scheduleAppRefresh()
let fetchTask = Task {
do {
let data = try await APIClient.shared.fetchLatestFeed()
await FeedStore.shared.update(with: data)
task.setTaskCompleted(success: true)
} catch {
task.setTaskCompleted(success: false)
}
}
// CRITICAL: Handle expiration -- system can revoke time at any moment
task.expirationHandler = {
fetchTask.cancel()
task.setTaskCompleted(success: false)
}
}
BGProcessingTask のパターン
メンテナンス、データ処理、クリーンアップを行うための、長時間(数分)のタスクです。デバイスがアイドル状態のときに実行され、外部電源を必要とする場合があります。同じ earliestBeginDate の下限ルールが適用されます。
func scheduleProcessingTask() {
let request = BGProcessingTaskRequest(
identifier: "com.example.app.db-cleanup"
)
request.requiresNetworkConnectivity = false
request.requiresExternalPower = true
request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 60)
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule processing task: \(error)")
}
}
func handleDatabaseCleanup(task: BGProcessingTask) {
scheduleProcessingTask()
let cleanupTask = Task {
do {
try await DatabaseManager.shared.purgeExpiredRecords()
try await DatabaseManager.shared.rebuildIndexes()
task.setTaskCompleted(success: true)
} catch {
task.setTaskCompleted(success: false)
}
}
task.expirationHandler = {
cleanupTask.cancel()
task.setTaskCompleted(success: false)
}
}
BGContinuedProcessingTask(iOS 26 以降)
ユーザー操作によってフォアグラウンドで開始され、バックグラウンドでも実行を続けるタスクです。システムはライブアクティビティで進捗を表示します。ProgressReporting に準拠します。
利用可能状況: iOS 26.0 以降、iPadOS 26.0 以降
BGAppRefreshTask や BGProcessingTask とは異なり、このタスクはフォアグラウンドからすぐに開始します。リソースが逼迫するとシステムによって終了される場合があり、進捗の報告が最小限のタスクが優先的に終了されます。ユーザーまたはシステムによるキャンセルに備えて expirationHandler を設定し、実行中の処理をキャンセルして、完了を報告する前に部分的な出力をクリーンアップします。
import BackgroundTasks
func startExport() {
// Register the task handler at app launch, not here.
// BGTaskScheduler requires registration before app launch completes.
let jobID = UUID().uuidString
let request = BGContinuedProcessingTaskRequest(
identifier: "com.example.app.export.\(jobID)",
title: "Exporting Photos",
subtitle: "Processing 247 items"
)
// Use a permitted base wildcard identifier: com.example.app.export.*
// earliestBeginDate is ignored for continued processing requests.
// .queue: begin as soon as possible if can't run immediately
// .fail: fail submission if can't run immediately
request.strategy = .queue
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not submit continued processing task: \(error)")
}
}
func performExport(task: BGContinuedProcessingTask) async {
let items = await PhotoLibrary.shared.itemsToExport()
let progress = task.progress
progress.totalUnitCount = Int64(items.count)
for (index, item) in items.enumerated() {
if Task.isCancelled { break }
await PhotoExporter.shared.export(item)
progress.completedUnitCount = Int64(index + 1)
// Update the user-facing title/subtitle
task.updateTitle(
"Exporting Photos",
subtitle: "\(index + 1) of \(items.count) complete"
)
}
task.setTaskCompleted(success: !Task.isCancelled)
}
GPU 処理を行う場合は、対応状況を確認し、Background GPU Access(com.apple.developer.background-tasks.continued-processing.gpu)を有効にします。
let supported = BGTaskScheduler.supportedResources
if supported.contains(.gpu) {
request.requiredResources = .gpu
}
バックグラウンド URLSession ダウンロード
アプリが一時停止または終了した後も続行するダウンロードには、URLSessionConfiguration.background を使用します。システムがプロセス外で転送を処理します。
class DownloadManager: NSObject, URLSessionDownloadDelegate {
static let shared = DownloadManager()
private lazy var session: URLSession = {
let config = URLSessionConfiguration.background(
withIdentifier: "com.example.app.background-download"
)
config.isDiscretionary = true
config.sessionSendsLaunchEvents = true
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
func startDownload(from url: URL) {
let task = session.downloadTask(with: url)
task.earliestBeginDate = Date(timeIntervalSinceNow: 60)
task.resume()
}
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL
) {
// Move file from tmp before this method returns
let dest = FileManager.default.urls(
for: .documentDirectory, in: .userDomainMask
)[0].appendingPathComponent("download.dat")
try? FileManager.default.moveItem(at: location, to: dest)
}
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: (any Error)?
) {
if let error { print("Download failed: \(error)") }
}
}
アプリの再起動に対応するため、システムの完了ハンドラーを保存して呼び出します。
// In AppDelegate:
func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
backgroundSessionCompletionHandler = completionHandler
}
// In URLSessionDelegate — call stored handler when events finish:
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
Task { @MainActor in
self.backgroundSessionCompletionHandler?()
self.backgroundSessionCompletionHandler = nil
}
}
バックグラウンドプッシュのトリガー
サイレントプッシュ通知は、アプリを短時間だけ起動して新しいコンテンツを取得します。プッシュペイロードで content-available: 1 を設定します。
{ "aps": { "content-available": 1 }, "custom-data": "new-messages" }
APNs リクエストは apns-push-type: background と apns-priority: 5 を指定して送信します。バックグラウンドプッシュの配信は優先度が低く、保証されません。送信頻度は抑え、通常は 1 時間あたり 2、3 回までにします。
AppDelegate で処理します。
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler:
@escaping (UIBackgroundFetchResult) -> Void
) {
Task {
do {
let hasNew = try await MessageStore.shared.fetchNewMessages()
completionHandler(hasNew ? .newData : .noData)
} catch {
completionHandler(.failed)
}
}
}
Background Modes で「Remote notifications」を有効にし、登録します。
UIApplication.shared.registerForRemoteNotifications()
よくある間違い
1. Info.plist の識別子が不足している
// DON'T: Submit a task whose identifier isn't in BGTaskSchedulerPermittedIdentifiers
let request = BGAppRefreshTaskRequest(identifier: "com.example.app.refresh")
try BGTaskScheduler.shared.submit(request) // Throws .notPermitted
// DO: Add every identifier to Info.plist BGTaskSchedulerPermittedIdentifiers
// <string>com.example.app.refresh</string>
2. setTaskCompleted(success:) を呼び出していない
上記の標準的なアプリ更新または処理ハンドラーを使用します。成功、失敗、キャンセルの各経路で、完了を必ず 1 回だけ報告します。
3. expiration handler を無視している
同じ標準ハンドラーを使用して、実行中の処理をキャンセルし、expirationHandler から失敗を報告します。
4. スケジュールの頻度が高すぎる
スケジュールの各セクションで下限ルールを説明しています。数分単位の更新リクエストは避けてください。実際の起動時刻は引き続きシステムが決定します。
5. バックグラウンド実行時間に頼りすぎている
// DON'T: Start a 10-minute operation assuming it will finish
func handleRefresh(task: BGAppRefreshTask) {
Task { await tenMinuteSync() }
}
// DO: Design work to be incremental and cancellable
func handleRefresh(task: BGAppRefreshTask) {
let work = Task {
for batch in batches {
try Task.checkCancellation()
await processBatch(batch)
await saveBatchProgress(batch)
}
task.setTaskCompleted(success: true)
}
task.expirationHandler = {
work.cancel()
task.setTaskCompleted(success: false)
}
}
レビューチェックリスト
-
BGTaskSchedulerPermittedIdentifiersにすべてのタスク識別子が記載されている - 必須の
UIBackgroundModes(fetch、processing)が有効になっている - アプリの起動完了前にタスクが登録されている
- すべてのコードパスで
setTaskCompleted(success:)が呼び出されている -
expirationHandlerが設定され、実行中の処理をキャンセルする - ハンドラー内で次のタスクがスケジュールされている(再スケジュールパターン)
-
earliestBeginDateに妥当な間隔が設定され、あくまで目安として扱われている - バックグラウンド URLSession でデリゲートが使用されている(async/クロージャーではない)
- バックグラウンド URLSession のファイルが、戻る前に
didFinishDownloadingTo内で移動されている -
handleEventsForBackgroundURLSessionが完了ハンドラーを保存し、呼び出している - バックグラウンドプッシュのペイロードに
content-available: 1が含まれている - バックグラウンドプッシュの APNs リクエストで
apns-push-type: backgroundとapns-priority: 5が使用されている -
fetchCompletionHandlerが適切な結果とともに速やかに呼び出されている - BGContinuedProcessingTask が
ProgressReportingで進捗を報告している - 処理が段階的で、キャンセルに安全である(
Task.checkCancellation()) - タスクハンドラー内でブロッキングを伴う同期処理を行っていない
参考資料
- バックグラウンド URLSession のエッジケース、シミュレートした起動によるデバッグ、バックグラウンドプッシュの ベストプラクティスなど、詳しいパターンについては references/background-task-patterns.md を参照してください。
- BGTaskScheduler
- BGAppRefreshTask
- BGProcessingTask
- BGContinuedProcessingTask(iOS 26 以降)
- BGContinuedProcessingTaskRequest(iOS 26 以降)
- バックグラウンドタスクを使用したアプリの更新
- iOS および iPadOS での長時間実行タスクの実行