background-processing

background-processing

熱門

使用 BGTaskScheduler 在 iOS 上排程與執行背景工作。適用情境包括:註冊 BGAppRefreshTask 進行短暫背景擷取、BGProcessingTask 執行長時間維護、BGContinuedProcessingTask(iOS 26+)將前景啟動的工作延續至背景執行、背景 URLSession 下載,或背景推播通知。涵蓋 Info.plist 設定、到期處理、任務完成以及透過模擬啟動進行除錯。

932星標
47分支
更新於 2026/7/15
SKILL.md
唯讀
名稱
background-processing
描述

使用 BGTaskScheduler 在 iOS 上排程與執行背景工作。適用情境包括:註冊 BGAppRefreshTask 進行短暫背景擷取、BGProcessingTask 執行長時間維護、BGContinuedProcessingTask(iOS 26+)將前景啟動的工作延續至背景執行、背景 URLSession 下載,或背景推播通知。涵蓋 Info.plist 設定、到期處理、任務完成以及透過模擬啟動進行除錯。

背景處理

使用 BackgroundTasks 框架、背景 URLSession 與背景推播通知,在 iOS 上註冊、排程與執行背景工作。

目錄

Info.plist 設定

每個任務識別碼必須Info.plistBGTaskSchedulerPermittedIdentifiers 中宣告,否則 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>       <!-- BGAppRefreshTask 需要 -->
    <string>processing</string>  <!-- BGProcessingTask 需要 -->
</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 = 預設背景佇列
        ) { 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("無法排程應用程式重新整理:\(error)")
    }
}

func handleAppRefresh(task: BGAppRefreshTask) {
    // 在執行工作前先排程下一次重新整理
    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)
        }
    }

    // 關鍵:處理到期——系統隨時可能收回時間
    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("無法排程處理任務:\(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+)

由使用者操作在前景啟動,並在背景持續執行的任務。系統透過 Live Activity 顯示進度。符合 ProgressReporting 協定。

可用性: iOS 26.0+、iPadOS 26.0+

BGAppRefreshTaskBGProcessingTask 不同,此任務會立即從前景啟動。系統在資源壓力下可以終止它,並優先處理回報最少進度的任務。設定 expirationHandler 以處理使用者或系統取消,取消進行中的工作,並在回報完成前清理部分輸出。

import BackgroundTasks

func startExport() {
    // 在應用程式啟動時註冊任務處理常式,而非此處。
    // BGTaskScheduler 要求在應用程式啟動完成前註冊。
    let jobID = UUID().uuidString
    let request = BGContinuedProcessingTaskRequest(
        identifier: "com.example.app.export.\(jobID)",
        title: "匯出照片",
        subtitle: "正在處理 247 個項目"
    )
    // 使用允許的基礎萬用字元識別碼:com.example.app.export.*
    // earliestBeginDate 對持續處理請求無效。
    // .queue:若無法立即執行,則盡快開始
    // .fail:若無法立即執行,則提交失敗
    request.strategy = .queue

    do {
        try BGTaskScheduler.shared.submit(request)
    } catch {
        print("無法提交持續處理任務:\(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)

        // 更新使用者看到的標題/副標題
        task.updateTitle(
            "匯出照片",
            subtitle: "已完成 \(index + 1) / \(items.count)"
        )
    }

    task.setTaskCompleted(success: !Task.isCancelled)
}

對於 GPU 工作,請檢查支援並啟用背景 GPU 存取(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
    ) {
        // 在此方法回傳前將檔案從暫存目錄移出
        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("下載失敗:\(error)") }
    }
}

處理應用程式重新啟動——儲存並呼叫系統完成處理常式:

// 在 AppDelegate 中:
func application(
    _ application: UIApplication,
    handleEventsForBackgroundURLSession identifier: String,
    completionHandler: @escaping () -> Void
) {
    backgroundSessionCompletionHandler = completionHandler
}

// 在 URLSessionDelegate 中——事件完成時呼叫儲存的處理常式:
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-push-type: backgroundapns-priority: 5 發送 APNs 請求。背景推播傳遞的優先級較低且不保證送達;請保持發送頻率低,通常每小時不超過兩到三次。

在 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 識別碼

// 錯誤:提交的任務識別碼不在 BGTaskSchedulerPermittedIdentifiers 中
let request = BGAppRefreshTaskRequest(identifier: "com.example.app.refresh")
try BGTaskScheduler.shared.submit(request)  // 拋出 .notPermitted

// 正確:將每個識別碼加入 Info.plist 的 BGTaskSchedulerPermittedIdentifiers
// <string>com.example.app.refresh</string>

2. 未呼叫 setTaskCompleted(success:)

使用上述標準的應用程式重新整理或處理處理常式:每個成功、失敗與取消路徑都只回報一次完成。

3. 忽略到期處理常式

使用相同的標準處理常式,在 expirationHandler 中取消進行中的工作並回報失敗。

4. 排程過於頻繁

排程章節說明了下限規則。避免以分鐘為單位的重新整理請求;系統仍會選擇實際啟動時間。

5. 過度依賴背景時間

// 錯誤:假設 10 分鐘的操作會完成
func handleRefresh(task: BGAppRefreshTask) {
    Task { await tenMinuteSync() }
}

// 正確:將工作設計為增量且可取消
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
  • [ ] 已啟用所需的 UIBackgroundModesfetchprocessing
  • [ ] 任務已在應用程式啟動完成前註冊
  • [ ] 每個程式碼路徑都已呼叫 setTaskCompleted(success:)
  • [ ] 已設定 expirationHandler 並取消進行中的工作
  • [ ] 在處理常式內排程下一個任務(重新排程模式)
  • [ ] earliestBeginDate 使用合理的間隔,並視為提示
  • [ ] 背景 URLSession 使用委派(而非 async/閉包)
  • [ ] 背景 URLSession 檔案在 didFinishDownloadingTo 中回傳前已移出
  • [ ] handleEventsForBackgroundURLSession 儲存並呼叫完成處理常式
  • [ ] 背景推播酬載包含 content-available: 1
  • [ ] 背景推播 APNs 請求使用 apns-push-type: backgroundapns-priority: 5
  • [ ] fetchCompletionHandler 已及時呼叫並帶有正確結果
  • [ ] BGContinuedProcessingTask 透過 ProgressReporting 回報進度
  • [ ] 工作是增量且可安全取消(Task.checkCancellation()
  • [ ] 任務處理常式中沒有阻塞的同步工作

參考資料