Efficiency awaits: Background tasks in SwiftUI
Swift & UI 进阶 20m

高效等待:SwiftUI 中的后台任务处理

Efficiency awaits: Background tasks in SwiftUI

2022年6月6日

在 Apple 官方观看视频

一句话判断

SwiftUI 终于有了原生的后台任务 API,配合 Swift Concurrency 让后台调度代码的可读性提升了一个量级。

这场 Session 讲了什么

Apple 在 SwiftUI 中引入了一套全新的 BackgroundTask API,统一了 watchOS、iOS、tvOS、Mac Catalyst 和 Widgets 的后台任务处理方式。Session 以一个叫 Stormy 的示例应用贯穿全程——这是一个暴风雨天气拍照应用,会在正午检测天气,如果当天下雨就通知用户拍照上传。

核心工作流程分三步:

  1. 调度:在前台时通过 BGAppRefreshRequest 预约后台唤醒时间
  2. 执行:系统在指定时间唤醒应用,发送后台任务
  3. 完成:应用处理完任务后系统重新挂起

这套 API 的最大卖点是全面拥抱 Swift Concurrency。过去处理后台任务需要嵌套多层 completion handler 和可变状态,现在可以用线性的 async/await 流程来写。

值得深挖的点

后台任务的生存周期管理是这次 Session 的重点。系统给后台任务的运行时间是有限的,当时间快用完时系统会发出信号。如果你的应用在这个信号到来前没有标记任务完成,应用可能被系统终止,还会被降低后续后台任务的调度优先级。

Background URLSession 的配合使用解决了”网络请求没完成但时间到了”的窘境。把网络请求设为 background 模式,应用可以立即完成当前后台任务进入挂起,等网络请求完成时系统会再次唤醒应用给一个新的 URLSession background task。

跨平台一致性是这套 API 的设计目标。同一个 backgroundTask modifier 在所有 Apple 平台上行为一致,你学一次就能到处用。这对于同时维护 iOS 和 watchOS 应用的团队来说是实打实的效率提升。

代码片段

调度后台刷新的核心代码:

// 调度明天正午的后台刷新
func scheduleAppRefresh() {
    // 计算明天中午的时间
    let noonTomorrow = Calendar.current.date(
        byAdding: .day, value: 1, to: Date()
    )!
    
    let request = BGAppRefreshRequest()
    request.earliestBeginDate = noonTomorrow
    
    do {
        try BGTaskScheduler.shared.submit(request)
    } catch {
        print("无法调度后台刷新: \(error)")
    }
}

SwiftUI 中注册后台任务处理器:

@main
struct StormyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        // 注册后台任务处理器
        .backgroundTask(.appRefresh("com.example.stormy")) {
            // 继续调度明天的刷新
            scheduleAppRefresh()
            
            // 用 async/await 检查天气
            let stormy = await isStormy()
            
            if stormy {
                // 如果是暴风雨天气,发送通知
                await notifyForPhoto()
            }
            // 闭包返回时,系统自动标记任务完成
        }
    }
}

异步天气检查:

func isStormy() async -> Bool {
    let url = URL(string: "https://weather.example.com/current")!
    let (data, _) = try! await URLSession.shared.data(from: url)
    let weather = try! JSONDecoder().decode(Weather.self, from: data)
    return weather.isStormy
}

最佳实践

  • 任务链式调度:每次后台任务开始时先调度下一次,确保刷新不会中断
  • 优先使用 background URLSession:网络请求不要在后台任务中同步等待,用 background 模式让系统帮你管理网络生命周期
  • 闭包结束即完成:backgroundTask modifier 的闭包返回时系统自动标记任务完成,不需要手动调用完成处理器
  • 优雅处理取消:Swift Concurrency 的任务取消机制天然适配后台任务超时场景,用 Task.isCancelled 检查当前状态
  • 标识符要统一:调度请求和处理器中用同一个标识符字符串,系统才能正确匹配

还有什么值得关注

  • 这套 API 对 Widget 的后台刷新同样适用,做小组件开发时值得留意
  • Session 提到 iOS 应用在 Mac 上运行也支持这套 API,Mac Catalyst 适配成本进一步降低
  • URLSession 的 data(from:) async 版本比传统的 dataTask(with:completionHandler:) 简洁太多,后台网络代码的维护成本会显著下降
  • 对于需要精确时间调度的场景(比如每天固定时间检查),earliestBeginDate 只是一个”最早”时间,实际唤醒可能延后
WWDC 2022