3. Swift 版本迁移(Swift 3→4→5 等)

Swift 语言这几年更新确实快。从 3 到 4,再到 5,每次大版本都带来一堆语法变化和 API 调整。

我做过好几次 SDK 的迁移工作,每次都得把整个项目的 Swift 代码扫一遍。

说实话,如果你维护的是 SDK,版本迁移这件事躲不过去——用户会用不同版本的 Swift 编译你的库,你必须保证兼容。

3.1 为什么要关注 Swift 版本迁移?

很多同学觉得只要项目能跑就不升级。但在 SDK 开发里,这是不行的。

举个例子:你用 Swift 3 写的库,用户项目用的是 Swift 4.2,直接就编译不过。

迁移的核心意义有三点:

  • 兼容性 —— 让 SDK 能在多个 Swift 版本下正常使用。
  • 性能与安全 —— 新版本通常会引入 ABI 稳定、内存优化等改进。
  • 社区生态 —— 依赖的第三方库也在升级,你不升就会被卡住。
我的习惯:每次 Xcode 大版本更新,我会先建一个空项目跑一遍迁移工具,看看编译器提示了哪些废弃 API,心里有个底。

3.2 Swift 3 → 4:变化量最大的一次

Swift 4 虽然号称源代码兼容 3.2,但实际从 3.0 直接跳过来,改的东西非常多。

我记得那年夏天,光改 String 相关的代码就改了两天。

3.2.1 String 不再是集合

Swift 3 里 String 是 Collection,能直接遍历字符。Swift 4 去掉了这个继承,改成 String 只能通过 .characters 访问。

后来 Swift 4.2 又把 .characters 去掉了,变成直接操作 String。折腾了好几轮。

// Swift 3
let str = "Hello"
for c in str { ... }

// Swift 4
for c in str.characters { ... }

// Swift 4.2+
for c in str { ... }

3.2.2 @objc 推断被缩减

Swift 3 里很多 NSObject 方法会自动暴露给 Objective-C。Swift 4 为了减小二进制体积,收紧了规则。

你要手动加 @objc 或 @objcMembers。

// Swift 3 自动推断
class MyClass: NSObject {
    func doSomething() {}
}

// Swift 4 需要显式标记
@objcMembers class MyClass: NSObject {
    func doSomething() {}
}
我曾经踩过的坑:在 Swift 3 时代,我用 KVO 监听一个属性,迁移到 Swift 4 后突然不触发回调了。后来发现是因为那个属性没加 @objc dynamic,编译器的推断变了。检查全部 KVO 和 target-action 相关的代码,一条条补。

3.2.3 Dictionary 与 Array 的新姿势

Swift 4 引入了 Dictionary 的 mapValues、filter 等方法,数组的 shuffle 也是在那时加的。

另外,以前用 removeAll(keepingCapacity:) 保持容量,Swift 4 之后这个签名微调过,编译会报方法未找到。

3.3 Swift 4 → 5:ABI 终于稳了

Swift 5 最大的成就是 ABI 稳定性。这意味着:你用 Swift 5 编译的二进制,可以在没有 Swift 运行时环境的老系统上跑(iOS 12.2+)。

对于 SDK 开发者来说,这简直是福音。

不过,迁移本身也有一些需要注意的地方。

3.3.1 字符串插值变化

Swift 5 改了字符串插值协议。以前你写 "\(value)" 直接用,现在如果你自定义了 StringInterpolation 协议,必须适配新签名。

// Swift 4
extension String.StringInterpolation {
    mutating func appendInterpolation(_ value: MyType) { ... }
}

// Swift 5
extension String.StringInterpolation {
    mutating func appendInterpolation(_ value: MyType) { ... }
    // 方法名相同,但参数类型和协议结构变了
}

其实大部分普通代码不受影响,只有写了自定义插值逻辑的才会报错。

3.3.2 Result 类型入标准库

Swift 5 把 Result<Success, Failure> 引入了标准库。如果你在项目里自己定义了 Result 枚举,会有命名冲突。

// 自定义的 Result(Swift 4 常用)
enum Result<T> {
    case success(T)
    case failure(Error)
}

// 迁移到 Swift 5 后,需要改名字或用标准库的
我建议:在 Podfile 或 Package.swift 里明确指定 Swift 版本 swift_version = '5.0',这样编译时能尽早暴露问题。

3.3.3 标准库 API 清理

Swift 5 废弃了一批旧 API,比如 String.UTF8View 的某些构造器,还有 Collection 上的 cached 方法。

Xcode 的迁移工具能帮你自动替换大部分,但有些边缘情况还是要手动修。

3.4 跨版本迁移的通用策略

如果你要同时支持 Swift 3、4、5,那需要点技巧。我一般用 #if swift(>=4.0) 这样的条件编译。

编译条件 适用场景
#if swift(>=5.0) Swift 5 特有 API
#if swift(>=4.0) && <5.0 Swift 4 但非 5
#if !swift(>=4.0) Swift 3 或更低

不过这也容易让代码变得难以维护。我的做法是:尽量让 SDK 只支持最新的两个大版本,用户如果太老就建议升级。

3.5 迁移工作流与工具

  1. Xcode 迁移工具:Edit → Convert → To Current Swift Syntax。它很聪明,但有时会遗漏自定义桥接。
  2. 先单元测试:迁移前确保测试覆盖率够高。我吃过亏,迁移完一堆编译错误,测试一跑发现逻辑都变了。
  3. 分模块迁移:模块化项目可以逐个 target 做迁移,方便回滚。
  4. 检查公开 API 头文件:如果是混编项目,要确认 .h 文件里引用的 Swift 类型名没有变化。
曾经有一次,我迁移一个大型 SDK,工具自动改了一千多处。结果测试时发现某个公共枚举的名称被改了命名规则(从驼峰变成了帕斯卡),用户代码全都编译不过。最后还是靠 git diff 一个个恢复。

3.6 迁移中的常见坑

  • 可选绑定语法:Swift 4 开始,if let x = x 这种写法偶尔会被编译器认为 ambiguous,需要改成 if let x = self.x。
  • Selector 字符串:Swift 3 里允许 #selector(methodName),Swift 4 收紧了对方法可见性的检查,必须加 @objc。
  • Core Graphics 类型:有些 CGFloat 相关的 API 在 Swift 4 后从函数变成了属性,比如 ceil(x) → x.rounded(.up)。
  • NSError 桥接:Swift 3 里 Error 协议有默认行为,Swift 4 开始强制要求类型显式实现 Error 或 LocalizedError。

3.7 迁移决策流程图

下面这张图能帮你快速判断自己该从哪个版本开始迁移,以及每步需要注意的核心变化。

Swift 版本迁移决策流程 当前 Swift 版本 >= 4.0 ? 直接升到 5.0 注意 ABI 和插值 先升到 3.2 String、@objc 大改 迁移到 Swift 4.2 迁移到 Swift 5.0 Result、字符串插值 发布二进制 注意点 • 利用条件编译 • 测试全覆盖 • 分模块迁移

3.8 小结

Swift 版本迁移不可怕,但一定要有章法。

我的经验是:提前做好测试,逐步升级,善用工具。每次迁移后做一次全量回归,比事后修 bug 省时得多。

你想想看,如果 SDK 用户一集成就报几十个编译错误,那体验得多糟糕。