mcpField
B
background-processing

background-processing

github.com/dpearson2699開発1.2k3.9k 回インストール13 週間前に更新
リポジトリ

background-processing について

iOS 開発者向けに、BackgroundTasks、バックグラウンド URLSession、プッシュ通知を使った処理を解説するスキルです。設定やスケジュール、キャンセル処理、実装上の注意点を扱います。

主な機能

  1. 01BGTaskScheduler と識別子の設定
  2. 02更新タスクと処理タスクのスケジュール
  3. 03フォアグラウンド開始の継続タスク
  4. 04バックグラウンド URLSession によるダウンロード
  5. 05バックグラウンドプッシュによる更新の起動
  6. 06期限切れ、キャンセル、完了の処理

活用シーン

  1. 01アプリデータの定期更新
  2. 02デバイスのアイドル時のメンテナンス
  3. 03アプリ停止後も続けるダウンロード
  4. 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 の設定

すべてのタスク識別子は、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())
  • タスクハンドラー内でブロッキングを伴う同期処理を行っていない

参考資料

コメント