适配 Swift 3 的一点小经验和坑

今天下午,我提交了基于 Swift 3.0 的奇点 2.1.1 版本,主要是适配了 Swift 3.0 + 一些 Bug 修复。在适配 Swift 3.0 的过程中,我记录了一些常见的问题,相信所有在适配过程中的朋友都会遇到,于是总结这么一篇文章分享一下。

前言

奇点项目是一个小型的纯 Swift 的 iOS 项目,十余个第三方库 + 超两万行代码。它是我在 2014 年一开始就接触 Swift 以来的一个 Objective-C 项目用 Swift 重写的实验作品,后来把它做成了产品推出来。所以它存在的价值之一就是为了适配新一代 Swift 😂

所以 Swift 3.0 出来后,我咬咬牙,就决定立即开始适配它了。大概花了约一个晚上+一个白天的时间我就完成了对适配工作,同时淘汰了一些不兼容 Swift 3 的三方库,可以说是神清气爽。

那么下面我就开始一一列举适配 Swift 工作中遇到的问题和一些新习得的概念吧。

Any vs AnyObject

将项目里的 AnyObject 转成 Any 可能是大家遇到的第一件适配大事。如何解释这个变化呢?在 Swift 3 之前,我们可以写完一个项目都只用 AnyObject 来代表大多数实例,好像不用与 Any 类型打交道。但事实上,Any 和 AnyObject 是有明显区别的,因为 Any 可以代表 struct、class、func 等等几乎所有类型,而 AnyObject 只能代表 class 生成的实例。

那为什么之前我们在 Swift 2 里可以用 [AnyObject] 声明数组,并且在里面放 Int、String 等 struct 类型呢?这是因为 Swift 2 中,会针对这些 Int、String 等 struct 进行一个 Implicit Bridging Conversions,在 Array 里插入他们时,编译器会自动将其 bridge 到 Objective-C 的 NSNumber、NSString 等类型,这就是为什么我们声明的 [AnyObject] 里可以放 struct 的原因。

但在 Swift 3 当中,为了达成一个门真正的跨平台语言,相关提案将 Implicit Bridging Conversions 给去掉了。所以如果你要把 String 这个 struct 放进一个 [AnyObject] 里,一定要 as NSString,这些转换都需要显示的进行了——毕竟 Linux 平台默认没有 Objective-C runtime。这样各平台的表现更加一致。当然这是其中一个目标,具体可见 0116-id-as-any 和相关提案的讨论。

@discardableResult 的使用

在 Swift 3 编译器下,如果一个 func 返回了一个对象,而你没有使用它时,会有一个 WARNING。对于追求项目洁癖(不想看到 1 个 WARNING 和 1 个 ERROR)的人来说这是不能忍的,尽管你可能是故意不去使用它的。

这里有两种方法可以解决这个 WARNING。

第一种:在 func 定义的前面,加上 @discardableResult 的修饰符,代表可以不使用返回值,这样编译器就不会有警告了。在我们自己定义的 func 上基本上都可以这么做。

但是还会有一种情况,用了第三方库或者系统库返回的对象怎么办?那只能用第二种办法:

_ = navigationController?.popViewController(animated: true)

像这样通过 _ 来省略掉了。虽然比较难看,考虑到 Swift 是一门严格的语言,就忍忍吧。

Protocol 实现一定要在对应的 extension 里

以前写代码时会有不注意的地方,比如 UITableViewDelegate 和 UITableViewDataSource,我分别用 extension 来实现,但是在具体的实现中没注意,把 delegate 的一个方法放进了 dataSource 的 extension 去实现,项目也能完全正常运作。

但是在 Swift 3.0 下,如果你在一个 extension 里实现一个 protocol,那么这个 protocol 的方法一定要在这个 extension 里面能找到,而不能在另外一个 extension 里或者主 class 或 struct 里面。不然会有类似这样的警告:

Objective-C method 'tableView:canEditRowAt:' provided by method 'tableView(_:canEditRowAt:)' does not match the requirement's selector ('tableView:canEditRowAtIndexPath:')

这也是 Swift 3 编译变得更严格的一个表现。

Implicitly Unwrapped Optionals 的坑

在 Swift 2 的项目中,我们可能存在这样不是特别安全的代码:

var greetings: String!
greetings = "Hello"
print("\(greetings) 图拉鼎")

这里会输出:

Hello 图拉鼎

没有任何问题。但是在 Swift 3 中,因为 Optional 的安全机制起作用了,会变成:

Optional("Hello") 图拉鼎

这个结果不是我们想要的。从这点也可以看到,Swift 3 的 IUO 行为变得更安全了,默认会把 IUO 变成 Optional。如果想要达到和 Swift 2 一样的效果,就得用:

print("\(greetings!) 图拉鼎")

这时你也注意到了,Swift 始终在用 ! 提醒你用 IUO 不那么安全。

所以趁这个机会可以好好重构一下老代码吧~

转化器一些奇怪的坑

如果你是用 Xcode 的自动转换器来转 Swift 3 项目的,应该会遇到一些奇怪的坑,特别举例:

  • UIControlState.normal 会转换成 UIControlState(),还好其他状态不会这样,所以全局搜索一次再替换即可;
  • IndexPath 明明是一个去 NS 的 Foundation 类型了,但是自动转换后代码经常会有 (indexPath as NSIndexPath) 这样的东西,完全可以去掉;
  • 所有 NSNotification.Name 都可以重构成 Notification.Name,但是转换器并没有做这件事情。与其同时,自己用字符串定义的 NotificationName,现在可以统一基于 extension 去扩展啦。

后记

适配完奇点这个两万多行代码的项目后,实际上可以总结的远远不止上面这些,我只是挑了一些典型来讲。更何况,我只是完成了第一步「适配」而已,更多的工作,比如把 API 命名更加 Swift 3 化,充分利用 Swift 3 的一些特性来重构项目等等都还没有做。

所以在接下去的路上,应该还会去总结和学习一些东西吧。祝大家适配 Swift 3 愉快😅

欢迎使用图拉鼎和他的团队开发的作品

One Switch - 多功能开关工具

常驻 macOS 菜单栏的开关工具,可以快速开关 AirPods、睡眠模式、切换黑暗模式等。

3 Comments

wudb

Xcode8写swift3的时候,经常不提醒了,而且也不会高亮显示了,index变得基本没有用了,你遇到过了吧

Akring

适配最麻烦的地方还是第三方库,每到Swift更新大版本的时候都是对已有第三方库的一次大清洗,实在绕不过的必备库只能自己PR了,工作量爆表。

Alen

期待一下“充分利用 Swift 3 的一些特性来重构项目”的总结

Leave a Comment