使用 BGTaskScheduler 在 iOS 上排程與執行背景工作。適用情境包括:註冊 BGAppRefreshTask 進行短暫背景擷取、BGProcessingTask 執行長時間維護、BGContinuedProcessingTask(iOS 26+)將前景啟動的工作延續至背景執行、背景 URLSession 下載,或背景推播通知。涵蓋 Info.plist 設定、到期處理、任務完成以及透過模擬啟動進行除錯。
背景處理
使用 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> <!-- 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+
與 BGAppRefreshTask 和 BGProcessingTask 不同,此任務會立即從前景啟動。系統在資源壓力下可以終止它,並優先處理回報最少進度的任務。設定 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: background 和 apns-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中 - [ ] 已啟用所需的
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()) - [ ] 任務處理常式中沒有阻塞的同步工作
參考資料
- 請參閱 references/background-task-patterns.md 以了解擴充模式、背景 URLSession 邊緣案例、透過模擬啟動進行除錯,以及背景推播最佳做法。
- BGTaskScheduler
- BGAppRefreshTask
- BGProcessingTask
- BGContinuedProcessingTask(iOS 26+)
- BGContinuedProcessingTaskRequest(iOS 26+)
- 使用背景任務更新您的應用程式
- 在 iOS 和 iPadOS 上執行長時間任務




