WWDC Quick Look 💓 By SwiftGGTeam
Migrate your app to Swift 6

Migrate your app to Swift 6

观看原视频

Highlight

Swift 6 语言模式通过编译期强制执行数据隔离,阻止跨 actor 共享可变状态导致的数据竞争。


核心内容

你写了一个 App,用了 @MainActor、async/await、Sendable,看起来已经”安全”了。但编译器其实没有阻止你把一个 class 实例从 main actor 传给另一个 actor——两个 actor 同时持有同一个引用类型,就可能出现数据竞争,轻则崩溃,重则损坏用户数据。

Swift 6 语言模式解决的就是这个问题。它引入了完整的数据隔离强制执行:编译器会在编译期阻止所有不安全的跨 actor 共享状态。你新增功能或重构代码时,编译器替你把关,不会让新的并发 bug 悄悄溜进来。

Session 以 CoffeeTracker App 为例演示了完整迁移流程。这个 App 之前已经迁移到了 Swift concurrency,但开启 Swift 6 的 Complete Concurrency Checking 后仍然冒出十几个警告。迁移策略是按 target 逐步推进:先对每个 target 开启 Complete Concurrency Checking(产生警告但不阻止编译),逐条解决后再开启 Swift 6 Language Mode(将警告升级为错误,锁定安全状态)。Session 反复强调一个原则:不要同时做大规模重构和开启 Swift 6,应该分开进行。


详细内容

迁移步骤:逐 target 推进

每个 target 的迁移遵循固定流程:(07:35

  1. 开启 Complete Concurrency Checking——项目仍处于 Swift 5 模式,但编译器会对所有可能在 Swift 6 下不安全的代码发出警告
  2. 逐条解决所有警告
  3. 开启 Swift 6 Language Mode——锁定改动,防止后续代码回退到不安全状态
  4. 进入下一个 target 重复上述步骤

Ben 建议从 App extension / UI 层开始迁移,而非底层框架。原因有二:UI 层大部分代码本身就在 main thread 上运行,警告较少;且上层迁移后,与未迁移框架的交互问题会暴露出来,可以用 @preconcurrency 等工具处理。

全局变量:var 改 let 是最常见的快速修复

开启 Complete Checking 后最常见的警告是全局 var。全局变量是共享可变状态,任何线程都能读写,天然存在数据竞争风险。编译器提供三种修复方式:(13:38

// 选项 1:var 改 let——Logger 是 Sendable 类型,let 声明后不可变,多线程安全
let logger = Logger(
    subsystem:
        "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView",
    category: "Root View")

// 选项 2:用 @MainActor 隔离——所有访问必须在 main actor 上
@MainActor var logger = Logger(
    subsystem:
        "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView",
    category: "Root View")

// 选项 3:nonisolated(unsafe)——绕过检查,由你自己保证安全
nonisolated(unsafe) var logger = Logger(
    subsystem:
        "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView",
    category: "Root View")

关键点:

  • let 是首选方案——Sendable 类型一旦声明为 let,就不存在共享可变状态的问题
  • @MainActor 适合确实需要可变但只从 main actor 访问的全局变量
  • nonisolated(unsafe) 是最后手段,它把安全责任交给了你自己,编译器不再检查;后续应回过头用正确架构替代它

Ben 特别提到,Swift 的全局变量是延迟初始化且原子创建的(16:29),不存在两个线程同时首次访问导致创建两个实例的问题。

跨 actor 发送数据:让类型遵循 Sendable

当 main actor 上的 CoffeeData 向另一个 actor 上的 CoffeeDataStore 发送 [Drink] 数组时,编译器发出警告:发送 self.currentDrinks 可能导致数据竞争(30:38)。解决方式是让 Drink 遵循 Sendable:(33:29

public struct Drink: Hashable, Codable, Sendable {
    public let mgCaffeine: Double
    public let date: Date
    public let uuid: UUID
    public let type: DrinkType?
    // ...
}

关键点:

  • Drink 是 struct,所有属性都是 let 的值类型,天然符合 Sendable 语义
  • internal 类型 Swift 会自动推断 Sendable,但 public 类型必须显式声明——因为 Sendable 是对调用方的承诺,你可能未来需要内部可变状态,不想提前锁定
  • DrinkSendable 一行改动消除了三处警告——寻找这类”一处改动消灭大量警告”的根因是高效迁移的关键

加上 Sendable 后,DrinkType 也需要同步标记为 Sendable:(35:04

public enum DrinkType: Int, CaseIterable, Identifiable, Codable, Sendable {
  // ...
}

如果 DrinkType 是一个无法遵循 Sendable 的 Objective-C 类型,可以用 nonisolated(unsafe) 标注该属性作为过渡方案,但这意味着你承担了安全责任。

处理未迁移框架的 protocol 隔离问题

当你的 @MainActor 类型需要遵循一个未标记 actor 隔离的 protocol 时,编译器会报错:Main actor-isolated 方法无法满足 nonisolated 的 protocol requirement(9:38)。Session 展示了两种解决方式:

方式一——@preconcurrency 标注 conformance:(25:21

extension Recaffeinater: @preconcurrency CaffeineThresholdDelegate {
    public func caffeineLevel(at level: Double) {
        if level < minimumCaffeine {
            // TODO: alert user to drink more coffee!
        }
    }
}

关键点:

  • @preconcurrency 等价于用 MainActor.assumeIsolated 包裹方法体——它告诉编译器”我知道这个回调会在当前 actor 上执行”
  • 如果运行时回调实际不在 main actor 上,程序会 trap(而非产生难以追踪的数据竞争)
  • 当底层框架也迁移到 Swift 6 并给 protocol 加了 @MainActor 后,@preconcurrency 会产生”不再需要”的警告,直接删除即可(26:50

方式二——手动用 nonisolated + MainActor.assumeIsolated:(24:15

nonisolated public func caffeineLevel(at level: Double) {
    MainActor.assumeIsolated {
        if level < minimumCaffeine {
            // TODO: alert user to drink more coffee!
        }
    }
}

这等价于 @preconcurrency 的手动展开版,适合需要更细粒度控制的场景。

CoreLocation delegate 回调的隔离处理

CoreLocation 的 CLLocationManagerDelegate 回调线程取决于创建 CLLocationManager 的线程——这是一个编译期无法静态检查的动态属性。Session 的做法是把整个 delegate 类放到 @MainActor 上,确保 manager 在 main thread 创建,回调也就在 main thread:(39:32

@MainActor
class CoffeeLocationDelegate: NSObject, CLLocationManagerDelegate {
  var location: CLLocation?
  var manager: CLLocationManager!

  var latitude: CLLocationDegrees? { location?.coordinate.latitude }
  var longitude: CLLocationDegrees? { location?.coordinate.longitude }

  override init () {
    super.init()
    manager = CLLocationManager()
    manager.delegate = self
    manager.startUpdatingLocation()
  }

  nonisolated func locationManager (
    _ manager: CLLocationManager,
    didUpdateLocations locations: [CLLocation]
  ) {
    MainActor.assumeIsolated { self.location = locations.last }
  }
}

关键点:

  • 类标注 @MainActor 后,CLLocationManager 在 main thread 上创建,CoreLocation 据此将回调也派发到 main thread
  • locationManager(_:didUpdateLocations:) 标记为 nonisolated,因为 protocol 要求没有 actor 隔离
  • 内部用 MainActor.assumeIsolated 访问 main actor 上的属性,如果运行时不在 main actor 则 trap

核心启发

  • 做什么:先开启 Complete Concurrency Checking 再逐条修警告,而非手动审计 为什么值得做:编译器会精确指出哪些代码不安全,比人工逐行排查效率高得多。开启后项目仍能正常编译运行,不阻塞交付。 怎么开始:在 Xcode 中选中 target -> Build Settings -> 搜索 “Swift Concurrency Checking” -> 设为 Complete。

  • 做什么:优先把全局 var 改成 let 为什么值得做:这是迁移中数量最多、修复最简单的警告类型。Session 中 CoffeeKit 的 11 个警告里,7 个是全局 var,一行 let 替换即可消除。 怎么开始:开启 Complete Checking 后,按警告列表逐一检查,把不会被重新赋值的全局 var 改为 let

  • 做什么:给跨 actor 传递的 public 值类型加 Sendable 为什么值得做:一个 Sendable 标注往往能消除多处 “Sending … risks causing data races” 警告。Session 中 DrinkSendable 一行改了三处警告。 怎么开始:定位 “Sending … risks causing data races” 警告中涉及的类型,如果该类型是 struct/enum 且所有属性都是值类型或 Sendable 类型,直接加 Sendable 遵循。

  • 做什么:对未迁移框架的 protocol conformance 使用 @preconcurrency 为什么值得做:当你确认回调实际会在某个 actor 上执行,但 protocol 还没标注隔离时,@preconcurrency 是最简洁的声明方式。等底层框架迁移后,编译器会自动提醒你删除它。 怎么开始:遇到 “Main actor-isolated instance method cannot satisfy nonisolated protocol requirement” 警告时,在 conformance 声明处加 @preconcurrency


关联 Session

评论

GitHub Issues · utterances