axiom-background-processing-ref by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-background-processing-refiOS 后台执行的完整 API 参考,包含来自 WWDC 会议的代码示例。
相关技能 : axiom-background-processing (决策树、模式), axiom-background-processing-diag (故障排除)
<!-- 必需:列出所有任务标识符 -->
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.yourapp.refresh</string>
<string>com.yourapp.maintenance</string>
<!-- 动态标识符的通配符 (iOS 26+) -->
<string>com.yourapp.export.*</string>
</array>
<!-- 必需:启用后台模式 -->
<key>UIBackgroundModes</key>
<array>
<!-- 用于 BGAppRefreshTask -->
<string>fetch</string>
<!-- 用于 BGProcessingTask -->
<string>processing</string>
</array>
import BackgroundTasks
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// 必须在 didFinishLaunching 返回前注册
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.refresh",
using: nil // nil = 系统创建串行后台队列
) { task in
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.maintenance",
using: nil
) { task in
self.handleMaintenance(task: task as! BGProcessingTask)
}
return true
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
参数 :
forTaskWithIdentifier: 必须与 Info.plist 完全匹配(区分大小写)using: 处理程序回调的 DispatchQueue;nil = 系统创建一个launchHandler: 任务启动时调用;接收 BGTask 子类来自 WWDC 2019-707:
"通过在应用程序完成启动前注册启动处理程序来实现此功能"
在以下位置注册:
application(_:didFinishLaunchingWithOptions:) 中,在 return true 之前全天保持应用内容新鲜。系统根据用户使用模式启动应用。
约 30 秒(与传统的后台获取相同)
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.yourapp.refresh")
// earliestBeginDate = 最小延迟(非精确时间)
// 系统根据使用模式决定实际时间
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
do {
try BGTaskScheduler.shared.submit(request)
} catch BGTaskScheduler.Error.notPermitted {
// 设置中禁用了后台应用刷新
} catch BGTaskScheduler.Error.tooManyPendingTaskRequests {
// 此标识符的待处理请求过多
} catch BGTaskScheduler.Error.unavailable {
// 后台任务不可用(模拟器等)
} catch {
print("调度失败: \(error)")
}
}
// 应用进入后台时调度
func applicationDidEnterBackground(_ application: UIApplication) {
scheduleAppRefresh()
}
func handleAppRefresh(task: BGAppRefreshTask) {
// 1. 首先设置过期处理程序
task.expirationHandler = { [weak self] in
self?.currentOperation?.cancel()
}
// 2. 调度下一次刷新(连续模式)
scheduleAppRefresh()
// 3. 执行工作
let operation = fetchLatestContentOperation()
currentOperation = operation
operation.completionBlock = {
// 4. 发出完成信号(必需)
task.setTaskCompleted(success: !operation.isCancelled)
}
operationQueue.addOperation(operation)
}
| 属性 | 类型 | 描述 |
|---|---|---|
identifier | String | 必须匹配 Info.plist |
earliestBeginDate | Date? | 执行前的最小延迟 |
可延迟的维护工作(数据库清理、ML 训练、备份)。在系统友好时间运行,通常在夜间充电时。
数分钟(显著长于刷新任务)
func scheduleMaintenanceIfNeeded() {
// 仅在需要工作时调度
guard Date().timeIntervalSince(lastMaintenanceDate) > 7 * 24 * 3600 else {
return
}
let request = BGProcessingTaskRequest(identifier: "com.yourapp.maintenance")
// 对 CPU 密集型工作至关重要
request.requiresExternalPower = true
// 可选:需要网络进行云同步
request.requiresNetworkConnectivity = true
// 保持在一周内 — 更长时间可能被跳过
// request.earliestBeginDate = ...
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("调度失败: \(error)")
}
}
func handleMaintenance(task: BGProcessingTask) {
var shouldContinue = true
task.expirationHandler = {
shouldContinue = false
}
Task {
for item in workItems {
guard shouldContinue else {
// 过期处理程序被调用 — 保存进度并退出
saveProgress()
break
}
await processItem(item)
saveProgress() // 每个项目后设置检查点
}
task.setTaskCompleted(success: shouldContinue)
}
}
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
identifier | String | — | 必须匹配 Info.plist |
earliestBeginDate | Date? | nil | 最小延迟 |
requiresNetworkConnectivity | Bool | false | 等待网络连接 |
requiresExternalPower | Bool | false | 等待充电 |
"有史以来第一次,我们允许您在处理任务期间关闭该功能,以便在设备插电时充分利用硬件。"
当 requiresExternalPower = true 时,CPU 监控器(通常终止 CPU 密集型后台应用)将被禁用。
在应用进入后台后继续用户发起的工作,系统 UI 显示进度。来自 WWDC 2025-227。
不适用于:自动任务、维护、同步 — 必须由用户明确发起。
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<!-- 动态后缀的通配符 -->
<string>com.yourapp.export.*</string>
</array>
与其他任务不同,在用户发起操作时注册:
func userTappedExportButton() {
// 动态注册
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.export.photos"
) { task in
let continuedTask = task as! BGContinuedProcessingTask
self.handleExport(task: continuedTask)
}
// 立即提交
submitExportRequest()
}
func submitExportRequest() {
let request = BGContinuedProcessingTaskRequest(
identifier: "com.yourapp.export.photos",
title: "正在导出照片", // 在系统 UI 中显示
subtitle: "已完成 0/100 张照片" // 在系统 UI 中显示
)
// 策略: .fail = 如果现在无法启动则拒绝;.enqueue = 排队(默认)
request.strategy = .fail
do {
try BGTaskScheduler.shared.submit(request)
} catch {
// 显示错误 — 现在无法在后台运行
showError("现在无法在后台导出")
}
}
func handleExport(task: BGContinuedProcessingTask) {
var shouldContinue = true
task.expirationHandler = {
shouldContinue = false
}
// 强制:报告进度
// 没有进度更新的任务会被自动过期
task.progress.totalUnitCount = Int64(photos.count)
task.progress.completedUnitCount = 0
Task {
for (index, photo) in photos.enumerated() {
guard shouldContinue else { break }
await exportPhoto(photo)
// 更新进度 — 系统向用户显示
task.progress.completedUnitCount = Int64(index + 1)
}
task.setTaskCompleted(success: shouldContinue)
}
}
| 属性 | 类型 | 描述 |
|---|---|---|
identifier | String | 使用通配符时,可以有动态后缀 |
title | String | 在系统进度 UI 中显示 |
subtitle | String | 在系统进度 UI 中显示 |
strategy | Strategy | .fail 或 .enqueue(默认) |
// .fail — 如果无法立即启动则拒绝
request.strategy = .fail
// .enqueue — 如果无法启动则排队(默认)
// 任务可能稍后运行
// 检查后台任务是否可用 GPU
let supportedResources = BGTaskScheduler.shared.supportedResources
if supportedResources.contains(.gpu) {
// GPU 可用
}
在应用切换到后台时完成关键工作(约 30 秒)。用于状态保存、完成上传。
var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
func applicationDidEnterBackground(_ application: UIApplication) {
backgroundTaskID = application.beginBackgroundTask(withName: "保存状态") {
// 过期处理程序 — 立即清理
self.saveProgress()
application.endBackgroundTask(self.backgroundTaskID)
self.backgroundTaskID = .invalid
}
// 执行关键工作
saveEssentialState { [weak self] in
guard let self = self,
self.backgroundTaskID != .invalid else { return }
// 工作完成后立即结束任务
UIApplication.shared.endBackgroundTask(self.backgroundTaskID)
self.backgroundTaskID = .invalid
}
}
endBackgroundTask — 不要等待过期.onChange(of: scenePhase) { newPhase in
if newPhase == .background {
startBackgroundTask()
}
}
即使在应用终止后仍继续的大文件下载/上传。工作移交到系统守护进程。
lazy var backgroundSession: URLSession = {
let config = URLSessionConfiguration.background(
withIdentifier: "com.yourapp.downloads"
)
// 任务完成时重新启动应用
config.sessionSendsLaunchEvents = true
// 系统选择最佳时间(WiFi、充电时)
config.isDiscretionary = true
// 请求超时(非下载本身)
config.timeoutIntervalForRequest = 60
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
func downloadFile(from url: URL) {
let task = backgroundSession.downloadTask(with: url)
task.resume()
// 即使应用终止,工作仍继续
}
var backgroundSessionCompletionHandler: (() -> Void)?
func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
// 存储 — 在所有事件处理后调用
backgroundSessionCompletionHandler = completionHandler
}
extension AppDelegate: URLSessionDelegate, URLSessionDownloadDelegate {
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL
) {
// 必须立即移动文件 — 临时位置在返回后删除
let destination = getDestinationURL(for: downloadTask)
try? FileManager.default.moveItem(at: location, to: destination)
}
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
// 所有事件已处理 — 调用存储的完成处理程序
DispatchQueue.main.async {
self.backgroundSessionCompletionHandler?()
self.backgroundSessionCompletionHandler = nil
}
}
}
| 属性 | 默认值 | 描述 |
|---|---|---|
sessionSendsLaunchEvents | false | 完成时重新启动应用 |
isDiscretionary | false | 等待最佳条件 |
allowsCellularAccess | true | 允许蜂窝网络 |
allowsExpensiveNetworkAccess | true | 允许昂贵网络 |
allowsConstrainedNetworkAccess | true | 允许低数据模式 |
在调试器中暂停应用,然后执行:
// 触发任务启动
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.yourapp.refresh"]
// 触发任务过期
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.yourapp.refresh"]
在 Console.app 中过滤:
subsystem:com.apple.backgroundtaskscheduler
检查已调度的内容:
BGTaskScheduler.shared.getPendingTaskRequests { requests in
for request in requests {
print("待处理: \(request.identifier)")
print(" 最早开始: \(request.earliestBeginDate ?? Date())")
}
}
| 因素 | 如何检查 | 影响 |
|---|---|---|
| 电池电量极低 | 电池 < ~20% | 可自由裁量的工作暂停 |
| 低电量模式 | ProcessInfo.isLowPowerModeEnabled | 活动受限 |
| 应用使用情况 | 用户频繁打开应用? | 优先级更高 |
| 应用切换器 | 未滑动关闭? | 滑动关闭 = 无后台 |
| 后台应用刷新 | backgroundRefreshStatus | 关闭 = 无 BGAppRefresh |
| 系统预算 | 最近启动次数多? | 预算耗尽,每日补充 |
| 速率限制 | 请求太频繁? | 系统间隔启动 |
// 低电量模式
if ProcessInfo.processInfo.isLowPowerModeEnabled {
// 减少后台工作
}
// 监听变化
NotificationCenter.default.publisher(for: .NSProcessInfoPowerStateDidChange)
.sink { _ in
// 调整行为
}
// 后台应用刷新状态
switch UIApplication.shared.backgroundRefreshStatus {
case .available:
// 可以调度任务
case .denied:
// 用户已禁用 — 在设置中提示
case .restricted:
// MDM 或家长控制 — 无法启用
@unknown default:
break
}
switch ProcessInfo.processInfo.thermalState {
case .nominal:
break // 正常操作
case .fair:
// 减少密集型工作
case .serious:
// 最小化所有后台活动
case .critical:
// 立即停止非必要工作
@unknown default:
break
}
NotificationCenter.default.publisher(for: ProcessInfo.thermalStateDidChangeNotification)
.sink { _ in
// 响应热状态变化
}
{
"aps": {
"content-available": 1
},
"custom-data": "your-payload"
}
apns-priority: 5 // 可自由裁量 — 节能(推荐)
apns-priority: 10 // 立即 — 仅用于时间敏感
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
guard userInfo["aps"] as? [String: Any] != nil else {
completionHandler(.noData)
return
}
Task {
do {
let hasNewData = try await fetchLatestData()
completionHandler(hasNewData ? .newData : .noData)
} catch {
completionHandler(.failed)
}
}
}
"在窗口期内接收 14 次推送可能仅导致 7 次启动,保持约 15 分钟的间隔。"
静默推送有速率限制。不要期望每次推送都启动。
@main
struct MyApp: App {
@Environment(\.scenePhase) var scenePhase
var body: some Scene {
WindowGroup {
ContentView()
}
.onChange(of: scenePhase) { newPhase in
if newPhase == .background {
scheduleAppRefresh()
}
}
// 应用刷新处理程序
.backgroundTask(.appRefresh("com.yourapp.refresh")) {
scheduleAppRefresh() // 调度下一次
await fetchLatestContent()
// 闭包返回时任务完成(无需 setTaskCompleted)
}
// 后台 URLSession 处理程序
.backgroundTask(.urlSession("com.yourapp.downloads")) {
await processDownloadedFiles()
}
}
}
.backgroundTask(.appRefresh("com.yourapp.refresh")) {
await withTaskCancellationHandler {
// 正常工作
try await fetchData()
} onCancel: {
// 任务过期时调用
// 保持轻量 — 同步运行
}
}
.backgroundTask(.urlSession("com.yourapp.weather")) {
// 后台 URLSession 完成时调用
// 处理已完成的下载
}
| 类型 | 运行时间 | API | 使用场景 |
|---|---|---|---|
| BGAppRefreshTask | ~30秒 | submit(BGAppRefreshTaskRequest) | 新鲜内容 |
| BGProcessingTask | 数分钟 | submit(BGProcessingTaskRequest) | 维护 |
| BGContinuedProcessingTask | 扩展 | submit(BGContinuedProcessingTaskRequest) | 用户发起 |
| beginBackgroundTask | ~30秒 | beginBackgroundTask(withName:) | 状态保存 |
| 后台 URLSession | 根据需要 | URLSessionConfiguration.background | 下载 |
| 静默推送 | ~30秒 | didReceiveRemoteNotification | 服务器触发 |
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>your.identifiers.here</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string> <!-- BGAppRefreshTask -->
<string>processing</string> <!-- BGProcessingTask -->
</array>
// 启动
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"ID"]
// 过期
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"ID"]
WWDC : 2019-707, 2020-10063, 2022-10142, 2023-10170, 2025-227
文档 : /backgroundtasks, /backgroundtasks/bgtaskscheduler, /foundation/urlsessionconfiguration
技能 : axiom-background-processing, axiom-background-processing-diag
最后更新 : 2025-12-31 平台 : iOS 13+, iOS 26+ (BGContinuedProcessingTask)
每周安装量
93
仓库
GitHub 星标数
606
首次出现
2026年1月21日
安全审计
安装于
opencode79
claude-code74
codex73
gemini-cli72
cursor71
github-copilot69
Complete API reference for iOS background execution, with code examples from WWDC sessions.
Related skills : axiom-background-processing (decision trees, patterns), axiom-background-processing-diag (troubleshooting)
<!-- Required: List all task identifiers -->
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.yourapp.refresh</string>
<string>com.yourapp.maintenance</string>
<!-- Wildcard for dynamic identifiers (iOS 26+) -->
<string>com.yourapp.export.*</string>
</array>
<!-- Required: Enable background modes -->
<key>UIBackgroundModes</key>
<array>
<!-- For BGAppRefreshTask -->
<string>fetch</string>
<!-- For BGProcessingTask -->
<string>processing</string>
</array>
import BackgroundTasks
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Register BEFORE returning from didFinishLaunching
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.refresh",
using: nil // nil = system creates serial background queue
) { task in
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.maintenance",
using: nil
) { task in
self.handleMaintenance(task: task as! BGProcessingTask)
}
return true
}
Parameters :
forTaskWithIdentifier: Must match Info.plist exactly (case-sensitive)using: DispatchQueue for handler callback; nil = system creates onelaunchHandler: Called when task is launched; receives BGTask subclassFrom WWDC 2019-707:
"You do this by registering a launch handler before your application finishes launching "
Register in:
application(_:didFinishLaunchingWithOptions:) before return trueKeep app content fresh throughout the day. System launches app based on user usage patterns.
~30 seconds (same as legacy background fetch)
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.yourapp.refresh")
// earliestBeginDate = MINIMUM delay (not exact time)
// System decides actual time based on usage patterns
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
do {
try BGTaskScheduler.shared.submit(request)
} catch BGTaskScheduler.Error.notPermitted {
// Background App Refresh disabled in Settings
} catch BGTaskScheduler.Error.tooManyPendingTaskRequests {
// Too many pending requests for this identifier
} catch BGTaskScheduler.Error.unavailable {
// Background tasks not available (Simulator, etc.)
} catch {
print("Schedule failed: \(error)")
}
}
// Schedule when app enters background
func applicationDidEnterBackground(_ application: UIApplication) {
scheduleAppRefresh()
}
func handleAppRefresh(task: BGAppRefreshTask) {
// 1. Set expiration handler FIRST
task.expirationHandler = { [weak self] in
self?.currentOperation?.cancel()
}
// 2. Schedule NEXT refresh (continuous pattern)
scheduleAppRefresh()
// 3. Perform work
let operation = fetchLatestContentOperation()
currentOperation = operation
operation.completionBlock = {
// 4. Signal completion (REQUIRED)
task.setTaskCompleted(success: !operation.isCancelled)
}
operationQueue.addOperation(operation)
}
| Property | Type | Description |
|---|---|---|
identifier | String | Must match Info.plist |
earliestBeginDate | Date? | Minimum delay before execution |
Deferrable maintenance work (database cleanup, ML training, backups). Runs at system-friendly times , typically overnight when charging.
Several minutes (significantly longer than refresh tasks)
func scheduleMaintenanceIfNeeded() {
// Only schedule if work is needed
guard Date().timeIntervalSince(lastMaintenanceDate) > 7 * 24 * 3600 else {
return
}
let request = BGProcessingTaskRequest(identifier: "com.yourapp.maintenance")
// CRITICAL for CPU-intensive work
request.requiresExternalPower = true
// Optional: Need network for cloud sync
request.requiresNetworkConnectivity = true
// Keep within 1 week — longer may be skipped
// request.earliestBeginDate = ...
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Schedule failed: \(error)")
}
}
func handleMaintenance(task: BGProcessingTask) {
var shouldContinue = true
task.expirationHandler = {
shouldContinue = false
}
Task {
for item in workItems {
guard shouldContinue else {
// Expiration called — save progress and exit
saveProgress()
break
}
await processItem(item)
saveProgress() // Checkpoint after each item
}
task.setTaskCompleted(success: shouldContinue)
}
}
| Property | Type | Default | Description |
|---|---|---|---|
identifier | String | — | Must match Info.plist |
earliestBeginDate | Date? | nil | Minimum delay |
requiresNetworkConnectivity | Bool | false | Wait for network |
requiresExternalPower | Bool | false | Wait for charging |
"For the first time ever, we're giving you the ability to turn that off for the duration of your processing task so you can take full advantage of the hardware while the device is plugged in."
When requiresExternalPower = true, CPU Monitor (which normally terminates CPU-heavy background apps) is disabled.
Continue user-initiated work after app backgrounds, with system UI showing progress. From WWDC 2025-227.
NOT for : Automatic tasks, maintenance, syncing — user must explicitly initiate.
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<!-- Wildcard for dynamic suffix -->
<string>com.yourapp.export.*</string>
</array>
Unlike other tasks, register when user initiates action :
func userTappedExportButton() {
// Register dynamically
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.export.photos"
) { task in
let continuedTask = task as! BGContinuedProcessingTask
self.handleExport(task: continuedTask)
}
// Submit immediately
submitExportRequest()
}
func submitExportRequest() {
let request = BGContinuedProcessingTaskRequest(
identifier: "com.yourapp.export.photos",
title: "Exporting Photos", // Shown in system UI
subtitle: "0 of 100 photos complete" // Shown in system UI
)
// Strategy: .fail = reject if can't start now; .enqueue = queue (default)
request.strategy = .fail
do {
try BGTaskScheduler.shared.submit(request)
} catch {
// Show error — can't run in background now
showError("Cannot export in background right now")
}
}
func handleExport(task: BGContinuedProcessingTask) {
var shouldContinue = true
task.expirationHandler = {
shouldContinue = false
}
// MANDATORY: Report progress
// Tasks with no progress updates are AUTO-EXPIRED
task.progress.totalUnitCount = Int64(photos.count)
task.progress.completedUnitCount = 0
Task {
for (index, photo) in photos.enumerated() {
guard shouldContinue else { break }
await exportPhoto(photo)
// Update progress — system displays to user
task.progress.completedUnitCount = Int64(index + 1)
}
task.setTaskCompleted(success: shouldContinue)
}
}
| Property | Type | Description |
|---|---|---|
identifier | String | With wildcard, can have dynamic suffix |
title | String | Shown in system progress UI |
subtitle | String | Shown in system progress UI |
strategy | Strategy | .fail or .enqueue (default) |
// .fail — Reject if can't start immediately
request.strategy = .fail
// .enqueue — Queue if can't start (default)
// Task may run later
// Check if GPU available for background task
let supportedResources = BGTaskScheduler.shared.supportedResources
if supportedResources.contains(.gpu) {
// GPU is available
}
Finish critical work (~30 seconds) when app transitions to background. For state saving, completing uploads.
var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
func applicationDidEnterBackground(_ application: UIApplication) {
backgroundTaskID = application.beginBackgroundTask(withName: "Save State") {
// Expiration handler — clean up immediately
self.saveProgress()
application.endBackgroundTask(self.backgroundTaskID)
self.backgroundTaskID = .invalid
}
// Do critical work
saveEssentialState { [weak self] in
guard let self = self,
self.backgroundTaskID != .invalid else { return }
// End task AS SOON AS work completes
UIApplication.shared.endBackgroundTask(self.backgroundTaskID)
self.backgroundTaskID = .invalid
}
}
endBackgroundTask immediately when done — don't wait for expiration.onChange(of: scenePhase) { newPhase in
if newPhase == .background {
startBackgroundTask()
}
}
Large downloads/uploads that continue even after app termination. Work handed off to system daemon.
lazy var backgroundSession: URLSession = {
let config = URLSessionConfiguration.background(
withIdentifier: "com.yourapp.downloads"
)
// App relaunched when task completes
config.sessionSendsLaunchEvents = true
// System chooses optimal time (WiFi, charging)
config.isDiscretionary = true
// Timeout for requests (not the download itself)
config.timeoutIntervalForRequest = 60
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
func downloadFile(from url: URL) {
let task = backgroundSession.downloadTask(with: url)
task.resume()
// Work continues even if app terminates
}
var backgroundSessionCompletionHandler: (() -> Void)?
func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
// Store — call after all events processed
backgroundSessionCompletionHandler = completionHandler
}
extension AppDelegate: URLSessionDelegate, URLSessionDownloadDelegate {
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL
) {
// MUST move file immediately — temp location deleted after return
let destination = getDestinationURL(for: downloadTask)
try? FileManager.default.moveItem(at: location, to: destination)
}
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
// All events processed — call stored completion handler
DispatchQueue.main.async {
self.backgroundSessionCompletionHandler?()
self.backgroundSessionCompletionHandler = nil
}
}
}
| Property | Default | Description |
|---|---|---|
sessionSendsLaunchEvents | false | Relaunch app on completion |
isDiscretionary | false | Wait for optimal conditions |
allowsCellularAccess | true | Allow cellular network |
allowsExpensiveNetworkAccess | true | Allow expensive networks |
allowsConstrainedNetworkAccess |
Pause app in debugger, then execute:
// Trigger task launch
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.yourapp.refresh"]
// Trigger task expiration
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.yourapp.refresh"]
Filter Console.app:
subsystem:com.apple.backgroundtaskscheduler
Check what's scheduled:
BGTaskScheduler.shared.getPendingTaskRequests { requests in
for request in requests {
print("Pending: \(request.identifier)")
print(" Earliest: \(request.earliestBeginDate ?? Date())")
}
}
| Factor | How to Check | Impact |
|---|---|---|
| Critically Low Battery | Battery < ~20% | Discretionary work paused |
| Low Power Mode | ProcessInfo.isLowPowerModeEnabled | Limited activity |
| App Usage | User opens app frequently? | Higher priority |
| App Switcher | Not swiped away? | Swiped = no background |
| Background App Refresh | backgroundRefreshStatus | Off = no BGAppRefresh |
| System Budgets | Many recent launches? | Budget depletes, refills daily |
| Rate Limiting | Requests too frequent? | System spaces launches |
// Low Power Mode
if ProcessInfo.processInfo.isLowPowerModeEnabled {
// Reduce background work
}
// Listen for changes
NotificationCenter.default.publisher(for: .NSProcessInfoPowerStateDidChange)
.sink { _ in
// Adapt behavior
}
// Background App Refresh status
switch UIApplication.shared.backgroundRefreshStatus {
case .available:
// Can schedule tasks
case .denied:
// User disabled — prompt in Settings
case .restricted:
// MDM or parental controls — cannot enable
@unknown default:
break
}
switch ProcessInfo.processInfo.thermalState {
case .nominal:
break // Normal operation
case .fair:
// Reduce intensive work
case .serious:
// Minimize all background activity
case .critical:
// Stop non-essential work immediately
@unknown default:
break
}
NotificationCenter.default.publisher(for: ProcessInfo.thermalStateDidChangeNotification)
.sink { _ in
// Respond to thermal changes
}
{
"aps": {
"content-available": 1
},
"custom-data": "your-payload"
}
apns-priority: 5 // Discretionary — energy efficient (recommended)
apns-priority: 10 // Immediate — only for time-sensitive
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
guard userInfo["aps"] as? [String: Any] != nil else {
completionHandler(.noData)
return
}
Task {
do {
let hasNewData = try await fetchLatestData()
completionHandler(hasNewData ? .newData : .noData)
} catch {
completionHandler(.failed)
}
}
}
"Receiving 14 pushes in a window may result in only 7 launches, maintaining a ~15-minute interval."
Silent pushes are rate-limited. Don't expect launch on every push.
@main
struct MyApp: App {
@Environment(\.scenePhase) var scenePhase
var body: some Scene {
WindowGroup {
ContentView()
}
.onChange(of: scenePhase) { newPhase in
if newPhase == .background {
scheduleAppRefresh()
}
}
// App refresh handler
.backgroundTask(.appRefresh("com.yourapp.refresh")) {
scheduleAppRefresh() // Schedule next
await fetchLatestContent()
// Task completes when closure returns (no setTaskCompleted needed)
}
// Background URLSession handler
.backgroundTask(.urlSession("com.yourapp.downloads")) {
await processDownloadedFiles()
}
}
}
.backgroundTask(.appRefresh("com.yourapp.refresh")) {
await withTaskCancellationHandler {
// Normal work
try await fetchData()
} onCancel: {
// Called when task expires
// Keep lightweight — runs synchronously
}
}
.backgroundTask(.urlSession("com.yourapp.weather")) {
// Called when background URLSession completes
// Handle completed downloads
}
| Type | Runtime | API | Use Case |
|---|---|---|---|
| BGAppRefreshTask | ~30s | submit(BGAppRefreshTaskRequest) | Fresh content |
| BGProcessingTask | Minutes | submit(BGProcessingTaskRequest) | Maintenance |
| BGContinuedProcessingTask | Extended | submit(BGContinuedProcessingTaskRequest) | User-initiated |
| beginBackgroundTask | ~30s | beginBackgroundTask(withName:) | State saving |
| Background URLSession | As needed | URLSessionConfiguration.background | Downloads |
| Silent Push | ~30s | didReceiveRemoteNotification | Server trigger |
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>your.identifiers.here</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string> <!-- BGAppRefreshTask -->
<string>processing</string> <!-- BGProcessingTask -->
</array>
// Launch
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"ID"]
// Expire
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"ID"]
WWDC : 2019-707, 2020-10063, 2022-10142, 2023-10170, 2025-227
Docs : /backgroundtasks, /backgroundtasks/bgtaskscheduler, /foundation/urlsessionconfiguration
Skills : axiom-background-processing, axiom-background-processing-diag
Last Updated : 2025-12-31 Platforms : iOS 13+, iOS 26+ (BGContinuedProcessingTask)
Weekly Installs
93
Repository
GitHub Stars
606
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
opencode79
claude-code74
codex73
gemini-cli72
cursor71
github-copilot69
Jira 自然语言交互指南:CLI与MCP后端使用教程与最佳实践
529 周安装
产品定位框架 | April Dunford《Obviously Awesome》方法论实践指南
546 周安装
MCP Lark 飞书集成指南:配置环境变量、调用工具与 mcporter 使用教程
535 周安装
WordPress Interactivity API 完整指南:交互式区块开发、服务器端渲染与调试
535 周安装
Next.js 智能体技能编写指南:创建、优化与管理 AI 技能文件
537 周安装
Deepline 反馈工具 - 向开发团队发送错误报告与功能建议
574 周安装
| true |
| Allow Low Data Mode |