Swift 与 OC 混编注意事项

Author Avatar
XibHe 8月 31, 2020
  • 在其它设备中阅读本文章

为了使 Objective-C 代码更好的符合 Swift 的使用习惯,主要有以下几种处理方式:

  • 遵循编译器的某些规则
  • 在头文件里进行特殊标注
  • 用 Swift 做中间层,重新封装原有代码
  • 根据自己的喜好进一步优化

快速查看 Xcode 基于 OC 代码生成的 Swift 接口

在需要转换的 OC 类的 .h 头文件中,点击左上角的 Related Items 按钮,选择 Generated Interface 后,就会出现几种不同 Swift 版本的接口文件选择其中某个版本的 Swift 接口文件,如下图所示:

选择其中某个版本的 Swift 接口文件,最终自动生成对应版本的 Swift 接口 API

Objective-C 与 Swift 混编的一些问题:

1. 过多的隐式解析可选类型

默认情况下 Swift 会把 Objective-C 里的指针当做隐式解析可选类型,因为它认为这个值大部分情况下不会是 nil,但它也不完全确定。

这样做会导致两个问题:

  • 大量的隐式解析可选类型让代码变得意图模糊
  • 隐式解析的存在,导致了容错能力的下降。只要服务端回传的参数中有一个空字段或者类型错误,就有可能引发 Crash

解决以上问题的方式:在 OC 代码中使用 nonnull (不可为空)和 nullable (可为空)关键字,根据这两个关键字决定转换后的 Swift 是否使用隐式解析。除了 nonnull 和 nullable 以外,还有一对配合使用的宏 NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END 可以让我们的代码更清爽。(新建类会自带这一对宏)

2. 某些转换后的属性里 Any 定义过于模糊

可以通过在 OC 里添加 NSArray,NSDictionary 中存储的固定类来改善这个问题。例如,

-(void)setRecommendArray:(nullable NSArray<MCHomeGoodModel *> *)recommendArray andSpace:(CGFloat)space andsectionInset:(UIEdgeInsets)sectionInse;

转换为:

open func setRecommendArray(_ recommendArray: [MCHomeGoodModel]?, andSpace space: CGFloat, andsectionInset sectionInse: UIEdgeInsets)

3. 一些初始化构造器或者方法命名不够优雅

Swift 和 Objective—C 的命名风格是有所不同,例如 Swift 的 API 是由基名(previousMissionsFlown)和参数标签(by)组成的,⽽ Objective—C 基本上只有参数标签(previousMissionsFlownByAstronaut),没有单独的基名,所以基名的信息会包含在第⼀个参数标签⾥,这也导致了 Objective—C 的方法名会显得略长一些。

4. 对于数字处理统一使用 Int (重要!!!)

NSUInteger 是无符号的,即没有负数。在 OC 中多以 NSUInteger 返回 index 或者设置枚举类型

由于 NSUInteger 的大小会因架构不同而产生一些变化,Swift 采取的策略是在进⾏有符号运算时,要求开发者必须将⽆符号类型转换为有符号类型,如果 Swift 在处理⽆符号运算时,产⽣了负值,就会直接停⽌运算。

对于 Apple 自己的框架,他们设置了一个白名单用于将 NSUInteger 转换为 Int。对于开发者而言,决定权在我们自己手里,我们可以⾃⾏选择是否使⽤ NSInteger,但 Apple 的工程强烈推荐你这么做。或许在 Objective-C ⾥⾯差距不是很⼤,但在 Swift ⾥⾯很重要!

5. 将字符串类型的常量变得更有条理

在 Swift ⾥通常会把这些常量变成⼀个具有字符串原始值的枚举或者结构体,然后改变函数的入参类型,使其接受相应的枚举或者结构体类型。不加任何处理的 OC 代码无法将设置的多个常量聚拢在一起,无法生成 Swift 中的结构体。

在 typedef 后⾯加上 NS_STRING_ENUM 即可, 此时,原有的字符串常量将以结构体的⽅式导⼊到 Swift 中. (这条验证后没有效果)

6. 关于构造器的相关约定

这个约定的大体内容是这样的,将初始化器分为两类,designated 和 convenience。你需要覆盖所有 designated 初始化器,以便安全地继承 convenience 的初始化器。

这个约定和 Swift 里面的构造器约定十分相似,但它们有个本质的区别!OC 的这种构造器约定不是语⾔级别的强制规则,更多的是⼀个开发者之间的约定,例如 convenience 必须选择⼀个 designated 的接口,但实际上很多 OC 的类并没这么做,这也意味着如果有⼦类的话,如何正确构造它会成为⼀个头⼤的问题!

通常 designated 构造器会调⽤ [super init] 这个方法,而 convenience 构造器会调⽤⾃⾝的某个 designated 构造器。我们需要在 designated 构造器后面添加 NS_DESIGNATED_INITIALIZER, 对于 convenience 类型的构造器,你不需要做任何事情

注意事项

1. nonull 和 nullable 只能在方法和属性上使用,如果想拓展其使用场景,就需要直接调用这两关键字底层的内容,也就是 _Nonnull 和 _Nullable。

2. 对于数字处理统一使用 Int,工程中混编 MCRollingNoticeView 中 index 转化后为 UInt 类型,因其 OC 中使用了 NSUInteger 类型。转化后可能导致的越界问题。(注意!!!)

3. 除了关注父类的 designated 构造器,开发者也需要关注 convenience 类型的构造器。

总结

虽然查看编译器生成的 Swift 头文件是一个好的方法。但⽣ 成的接口并不是全部,真正重要的是使用者在实际使⽤过程中写出的调⽤代码。所以当我们在思考如何打造一个更适合 Swift 使用的接口时,不光要看看⽣成的接口。也应该考虑实际的使用场景。

参考文档

让 Objective-C 框架与 Swift 友好共存的秘籍

通过在 Objective-C 中设置 api 的特定宏来定制 Objective-C 代码并将其导入 Swift

WWDC2020 Refine Objective-C frameworks for Swift

–EOF–

本篇文章主要参照了 让 Objective-C 框架与 Swift 友好共存的秘籍 转载请保留原作者文章链接,谢谢!