将你的 App 迁移到 Swift 6
Migrate your app to Swift 6
2024年6月10日
一句话判断
如果你的 Swift App 还没开 Swift 6 模式,现在是时候了——编译器会告诉你所有潜在的数据竞争问题,而且迁移过程可以按模块逐步推进。
这场 Session 讲了什么
Swift 6 语言模式在 WWDC 2024 正式发布,核心能力是完整的数据隔离(data isolation)强制执行。在这之前,即使你用了 actor 和 async/await,编译器也不会阻止你把一个 class 实例在多个 actor 之间传递——共享可变状态仍然可能导致数据竞争。Swift 6 模式下,编译器会在编译期把这类问题全部标记出来。
Session 以 CoffeeTracker App 为例,展示了完整的迁移流程。这个 App 之前已经迁移到了 Swift concurrency(使用了 @MainActor、async/await、Sendable),看起来”应该”是安全的,但实际上编译器仍然允许不安全的代码存在。启用 Swift 6 模式后,编译器会变成一个”严格的代码审查者”,指出每一个可能的并发安全问题。
迁移策略是按模块逐步推进:先对每个 target 开启 Complete Concurrency Checking(产生警告但不阻止编译),解决所有警告后再开启 Swift 6 模式(将警告升级为错误)。Session 强调了一个重要原则:不要同时做大规模重构和开启 Swift 6,应该分开进行。
值得深挖的点
Swift 6 迁移的实际收益
很多人觉得”我的 App 已经用了好几年了,数据竞争早就修完了,迁移没意义”。Session 明确反驳了这种想法。迁移的真正价值不在于修已知 bug,而在于保护未来写的新代码。
当你新增功能或重构代码时,Swift 6 编译器会实时检查每一段新代码是否引入了数据竞争。这意味着你可以放心地增加并发——比如把某个同步操作改成异步,或者把某个 class 改成 actor——编译器会在出问题的第一时间阻止你。没有 Swift 6 模式的话,这类问题通常只在线上以难以复现的偶发崩溃出现。
对于维护公开 Swift Package 的开发者,尽早迁移到 Swift 6 还有一个额外好处:你的用户在迁移自己的项目时,不用再花时间处理依赖库的并发警告。
Complete Checking 到 Swift 6 的渐进式路径
Session 推荐的迁移路径是分三步走:先在 Build Settings 中对单个 target 开启 “Strict Concurrency Checking” 设为 “Complete”;然后逐个修复所有警告;最后将 Swift Language Version 切换为 6。每完成一个 target 再进入下一个。
这个渐进式路径的关键在于:Complete Checking 产生的是警告,不会阻止编译和运行。你可以在不影响日常开发节奏的情况下逐步推进。而且编译器的诊断信息非常具体,会直接指出哪行代码有并发安全问题、为什么有问题、建议怎么改。Session 形容它”像一个 pair programmer 在帮你找 bug”。
代码片段
场景一:开启 Complete Concurrency Checking 并解读警告
// 在 Xcode 中:Build Settings -> Swift Compiler - Language
// 将 "Strict Concurrency Checking" 设为 "Complete"
// 或者在 Package.swift 中:
target(
name: "CoffeeKit",
swiftSettings: [.unsafeFlags(["-strict-concurrency=complete"])]
)
// 开启后,编译器会对以下代码发出警告:
class CoffeeStore {
var servings: [Coffee] = [] // ⚠️ Shared mutable state
func addServing(_ coffee: Coffee) {
servings.append(coffee)
}
}
// 修复方案:标记为 actor 或 @MainActor
@MainActor // 确保只在主线程访问
class CoffeeStore {
var servings: [Coffee] = []
func addServing(_ coffee: Coffee) {
servings.append(coffee)
}
}
// 坑点:如果 Coffee 不是 Sendable 的,跨 actor 传递时会报错
// 需要同时让 Coffee 遵循 Sendable 协议
struct Coffee: Sendable { // struct 自动满足 Sendable
let name: String
let caffeine: Double
}
场景二:处理非 Sendable 类型的跨 actor 传递
// 场景:HealthKit 回调在任意线程,需要把数据传到主 actor
// Swift 6 会阻止直接传递非 Sendable 类型
// 错误写法:
func onHealthData(_ data: HealthSnapshot) {
// ⚠️ HealthSnapshot 不是 Sendable,不能跨 actor 传递
Task { @MainActor in
viewModel.update(with: data) // 编译器会报错
}
}
// 修复方案一:让 HealthSnapshot 遵循 Sendable
struct HealthSnapshot: Sendable {
let heartRate: Double
let timestamp: Date
}
// 修复方案二:使用 @unchecked Sendable(当确认线程安全时)
final class HealthCache: @unchecked Sendable {
private let lock = NSLock()
private var _data: [HealthSnapshot] = []
var data: [HealthSnapshot] {
lock.lock()
defer { lock.unlock() }
return _data
}
}
// 坑点:@unchecked Sendable 绕过了编译器检查
// 确保你真的保证了线程安全,否则等于把问题藏起来了
场景三:逐步迁移多个 target
# 迁移步骤(以 CoffeeTracker App 为例):
# 1. 确认 App 在 Xcode 16 下能正常编译
# Swift 6 编译器保证源码兼容性,现有代码应该能直接编译
# 2. 对底层框架(CoffeeKit)开启 Complete Checking
# Build Settings -> Swift Compiler - Language
# -> Strict Concurrency Checking = Complete
# 3. 修复 CoffeeKit 中的所有并发警告
# 常见修复:
# - class -> actor 或 @MainActor class
# - struct 添加 Sendable 遵循
# - closure 标记 @Sendable
# - 使用 nonisolated 关键字隔离方法
# 4. 将 CoffeeKit 的 Swift Language Version 切为 6
# 此时所有警告变为错误,确保不会再有新的并发问题引入
# 5. 对 App 主 target 重复步骤 2-4
# 6. 全部完成后,可以考虑做一轮整体重构
# 比如移除之前临时加的 nonisolated(unsafe) 等
最佳实践
- 迁移顺序:从底层依赖开始,逐层向上迁移。框架先迁移,App target 后迁移。因为底层框架迁移后,上层代码的 Sendable 约束更容易满足。
- 不要混合重构和迁移:这两个操作应该分开做。先完成 Swift 6 迁移(保持代码逻辑不变),再单独做架构重构。混合进行会让问题定位变得极其困难。
- nonisolated(unsafe) 是临时方案:Swift 6 提供了
nonisolated(unsafe)来暂时绕过检查,但它是过渡工具,不是最终解决方案。在迁移完成后应该回过头用正确的架构替代它。
还有什么值得关注
- Swift Package Index 网站可以查看主流开源库的 Swift 6 迁移进度,选依赖时可以参考
- Swift 6 的数据隔离不仅影响 App 代码,extension 和 framework target 也需要迁移
- Session 提到 Apple 内部的 iCloud Keychain、Photos、Notes 等服务都在用 Swift on Server,Swift 6 的安全性保障在大规模分布式系统中同样重要