大多数浏览器和
Developer App 均支持流媒体播放。
-
改进 Swift 的 Objective-C 框架
对你的 Objective-C 数据头进行微调,使其与 Swift 完美匹配。我们将为你展示如何将僵硬难用的 Objective-C 框架转化为舒适方便的 API。学习如何使用注释套件,用注释做出丰富的类型信息、更朗朗上口的名称,以及更好的 Swift 错误信息。你还将探索也许之前并不了解的 Objective-C 传统使用方式,要做出驯服易用的 Swift API,它们正是关键。 为了在此节中获得最佳教学效果,请预先熟悉 Swift 与 Objective-C 的相关内容。 要了解更多关于 Swift 与 Objective-C 的使用信息,请查看我们的开发者文档,以及 WWDC18 的“Xcode开发流程幕后”一节。
资源
-
下载
(你好 WWDC 2020)
大家好 欢迎来到 WWDC (改进 Swift 的 Objective-C 框架) 大家好 我是 Swift 编译器团队的 Brent 我今天要给大家讲一讲 如何让你的 Objective-C 框架 在 Swift 里顺畅运行 六年前的这个月 我们首次介绍了 Swift 自那以后 发生了令人惊奇的事情 你们当中的大多数人 都已经成了 Swift 开发者 但 Apple 的平台在很长一段时间内 都是只支持 Objective-C 的平台 你们当中很多人 当时都无法立刻采用 Swift 语言 所以很多人 现在仍然有很多 Objective-C 代码 越来越多地在 Swift 客户端上使用
我们之所以了解这一点 是因为 Apple 也是这种情况 我们可能是世界上 采用 Objective-C 框架最多的开发者 所以我们设计出 Swift 不只是为了 导入我们的 Objective-C 框架 也是为了把这些框架自动转译成 更为惯用的 Swift API 我们设计时采取了多种方式 通过将宏指令和关键词添加到标头文件 来对这个转译过程进行自定义
我们还内置了使用自定义的 Swift 代码 来扩展 Objective-C 框架的能力 以此包装已有的 API 或者 添加无法用 Objective-C 表示的新 API
最棒的是 我们做这些设计 为了我们自己 也是为了大家 这就是我今天要讲的主题 Apple Objective-C 框架会用一些技术 来提升其被导入 Swift 时的效果 我会把这些技术教给大家 为此 我会用到一个小框架 我把它叫做 SpaceKit 我们来看看这个框架吧
SpaceKit 是一个 Objective-C 框架 这个框架中的一些模型 API 用于描述 美国航空航天局早期粗略的太空计划 所以它能描述宇航员、任务和火箭等内容 如果看一下标头 就可以很清楚地看出 这是一个 Objective-C 框架 其中充满了 #import 指令 和 @interface 块 以及带有选择符的方法 但如果我把它导入 Swift 编译器会自动将其转译为 Swift API 而且看标头生成的接口 就能看到那个 API 在 Xcode 中的样子 我只需要打开编辑程序左上角的 “相关项目” 菜单
找到 “生成接口” 子菜单
然后请求 Swift 5 接口 因为 Xcode 12 中的 Swift 版本是 5.3 如果 Swift 有标头文件的话 那我最后看到的 基本上就是 Swift 标头文件的样子 大家能看到 它有这个类 有一些初始器、一些属性和一些方法 但我没看到这些声明的主体 只有接口 这是一个非常有价值的工具 可以了解到将该框架 导入到 Swift 中时 Swift 会看到什么内容
在这个视频中 我会展示很多 SpaceKit 生成的接口 还有这类伪标头
如果大家仔细在里面看一下就会发现 Swift 已经为大家做了一些很棒的东西 比如 这些参数以前在 Objective-C 中 都是 NSString 和 NSDate 对象 但编译器已经把它们桥接到了 你会在 Swift 里使用的结构中
它把 init 方法导入成了初始器 把方法名重写成了更接近 Swift 的样式 并把遵守 Objective-C 错误处理规范 的方法变成了抛出的字节 但这个框架也还有一些改进空间 API 布满了这些隐含的未包装可选项 在这些集合中 “AnyHashable” 类别中的 “Any” 很含糊
这种抛出方法 有时候会在不恰当的情况下抛出 而且这个方法的名称可能并不是很完美
与其它相比 这两个初始器看着出人意料的冗余 用 trycatch 的时候 这种 NSError 信息就会很难用
如果抛开对象不说 看看 SpaceKit 中 更接近 C-style 的内容 你就会发现还有更多工作需要做
这一类使用了 NS-Enum 所以它是 Swift 中非常好的枚举类型 但这些字符串常量的集合 可以进行相同的处理 其中一个常量已经直接消失了 这些自由浮动的实用功能 对 Swift 来说并不理想 而且在编程中使用这个 U-Int 结果 会非常痛苦 我会从头到尾讲一讲如何改进这些问题 我会给大家展示一下 如何在你的 API 中更精确地指定类别 从而帮助你的 Swift 客户端 正确使用你的框架 我会大概讲两个非常重要的 Objective-C 规范 都是 Swift 假定你的 API 会遵守的规范 我会教大家 如果Objective-C 无法存取 Swift 导入的东西 应该如何修正 我会帮你在你的框架上完成点睛之笔 把它变成优秀的 Swift 产品 好了 我们开始吧 先讲一下那些隐含的未包装可选项 这些可选项虚拟存在于 API 的各个位置 它们的存在导致 你的框架行为里非常重要的方面都未声明 那它们是从哪里来的? 我们怎么才能清除它们? Objective-C 指针类型 其中包括 id 类型和块 可能有有效值 或者也可能有零值 我们称之为 “null” 或 “nil” 也就是空值 这很像 Swift 的可选项类型 要么是有值 要么是空值 唯一的不同是 在 Objective-C 中 每个指针类型都是有效可选 而且每个非指针类型都是有效不可选 但当然 在很多时候 一项属性或方法不会真正处理空值输入 或者根本不会返回空值结果 所以 当 Swift 导入 Objective-C 指针类型时 会默认将其标记为隐含的未包装可选项 以此告诉你这个值可能是空值 但 Swift 不确定它是否确实是空值
很幸运 Objective-C 有两个为空性标注 非空与可空 这两者可以让你说明空值 对于特定属性、方法参数或方法结果来说 是否是一个实用值 Objective-C 不强制执行这些标注 它们只是记录你的意图 但 Swift 会获得这个信息 然后用其决定是否将一个类型定为可选项
通过编辑你的 Objective-C 标头文件 你就可以应用这些标注
在属性中 标注是属性特性列表中的一项 在方法、参数或结果类型中 标注就在类型名称前面
所以 假设你先从这个名称属性开始 你决定这个类的例子未必都有名称 所以你添加了可空的标注 而且生成接口的类型变成了可选项 很好 但之后你点击了“构建” 然后你开始看到出现了新的警告 别慌 你没有弄坏任何东西 只是 Xcode 注意到 你在这个标头里开始使用为空性标注了 所以它开始告诉你 还有哪些地方你还需要填上为空性标注
从而给你带来一个非常顺畅的工作流程 在标头的某个地方添加第一个标注 然后逐个清除掉警告 要么填上"可空" 要么填上“非空” 直到所有警告均已清除
一旦所有警告均已清除 你还应该做一步清理 即 在文件顶部 添加 NS_ASSUME_NONNULL_BEGIN 宏指令 在底部添加匹配端宏指令 然后删除顶部和底部之间所有的非空值 这只是清理了文件的一点点 以便无论你何时看到这些关键词之一 它都表明你会在 Swift 中得到可选项 在你添加这些时 你偶尔会遇到这些标注不起作用的时候 比如这个常量 如果你尝试把非空标注放在它前面 你就会遇到编译器错误 生成接口很可能根本就不会显示 我向大家展示的这些标注 适用于方法和属性 但在其它所有地方 比如常量、全局函数或代码块 你就必须转而使用这些标注的限定符版本 它们的开头是下划线和一个大写字母 它们能对 Objective-C 中所有地方的 所有指针类型起作用 对于二级指针类型 你也会需要这些限定符版本 在此情况下 你需要为指针的每一个级别 指定一个关键词 所以可以说 内部指针可以是空值 但外部指针不可能是空值 Swift 会正确嵌套可选项以及 不安全的可变指针类型 所以 回到我们的全局函数 我们使用下划线限定符 它就和我们之前想的一样 在 Swift 里变成了非可选项 你这么做的时候要小心 要确保你的标注准确无误 比如 你快速浏览了一下这个标头 然后决定 “每个任务都要有一个容器 我们做一个容器属性的非空值吧” 但后来发现 有一个任务没有启动容器 然后你仿照模型将容器设置成了空值 换句话说 之前的非空标注是错的 那如果 Objective-C 针对一个 Swift 认为不可能是可选项的值 返回了 “空值” 那会发生什么? 如果它是 Objective-C 的 NSString 或 NSArray 那就是个特殊情况 你会得到一个空的 Swift 字符串或数组 这里可能就会有一个问题 因为 SpaceKit 预期字串符 会等于它的 SKCapsule 的一个常量 而这些常量没有一个等于空字符串 所以最后的结果是 有一个值你可能没有正确地处理
对于其它类型来说 情况可能会奇怪得多 最后你可能会得到一个无效对象 而无效对象一般只会由于不安全操作产生
如果它是一个 Objective-C 对象 你甚至可能都不会注意到 因为 Objective-C 方法会忽略空值 但有些情况下 你就会遇到空的指针解除引用 或者其它意料之外的行为 编译器不会针对会发生的事情做任何许诺 所以转到发布模式或者更改 Xcode 版本 可能会改变这种漏洞的表现
重要的一点是 当你在标头中 编写一些不可能是空值的代码时 Swift 不会对其进行强制解包装 所以在返回空值的地方 你不会看到崩溃的现象发生 Swift 不会预测你的 Objective-C 标头 它相信标头告诉它的信息 好消息是 Objective-C 编译器 和 Clang 静态分析工具 也会看为空性标注 并且能在你的 Objective-C 代码中 指出很多违规之处 所以如果你说一个东西不可能是空值 但 Objective-C 代码将其设为了空值 这些工具可能就会告诉你有地方出错了 你添加完为空性标注后 最好在框架的实现文件以及你能访问的 所有 Objective-C 客户端里 找找有没有新的警告 或静态分析工具结果 那些结果可能会显示 你可能做了错误的标注 或者可能告诉你 你之前不知道的小漏洞 但假设你看到了一些警告或者分析结果 但你没办法判断它们是不是真会发生 或者你只是有一些非常复杂的旧代码 而你不知道是否会有一些 会返回空值的边界情况 你这时候应该怎么办? 我之前提到了非空和可空标注 其实还有第三个选择 也就是 “不确定是否为空” 它会让 Swift 将值导入为 隐含的未包装可选项 在 Swift 中 如果你会在一个地方用隐含未包装可选项 你都应该在这些地方用“不确定是否为空” 比如那些只在一个对象生命周期的最早期 是空值的值 但如果你认为一个 API 无法返回空值 但同时你又不确定 这时候你也可以用“不确定是否为空” 这样的话 Swift 客户端仍然可以在 不解包装的情况下使用结果 但如果框架最后真的返回了空值 使用的站点确实就会直接崩溃 而不会在之后得到一些 也许看起来不可能发生的不正当行为 我们已经浏览了整个项目了 并且添加了所有可空注释 太好了 接下来我们要处理的是这个 “Any” 的数组 以及桥接得不太理想的集合 Objective-C 支持一种通用语法 这种语法和 Swift 的语法很像 因此如果在 Objective-C 中把它做成 一个 NSArray 的 SKAstronauts 那么会在 Swift 中会得到一个 SKAstronauts 的 Swift 数组 同理 NSSet 和 NSDictionary 也可以 这么做 这样一来 能够改进许多 API 接下来 让我们看一下这个项目的另一块 这里有一个叫做 SKRocketStageCount 的函数 顾名思义 它会返回一个 count 对象 由于这一 count 对象不能为负 因此它是作为 NSUInteger 返回的 也就是说 在 Swift 中 它返回了一个 UInt 这样一来 这个函数打破了一个 Swift 的规范 当用一个整数来代表位的集合 并且你想对这些位进行位运算 或做一些可能会被有符号运算 妨碍的计算时 在 Objective-C 和 Swift 中 使用无符号类型是规范 通常来说 当你在这一层面工作时 你关心的是值中的确切位数 由于 NSUInteger 的大小随架构而变 因此这种使用方法不太常见 相对而言 在 Objective-C 中使用 NSUInteger 的主要原因 是用来表示一个数字的值 在任何情况下均不为负数 Objective-C 通过自动转换 和精心设计的溢出行为来实现这种风格 但是这些性能可能会造成严重的安全漏洞 因此 Swift 并不包含这些性能 相反地 Swift 要求 若想进行有符号运算 且想实现若无符号运算产生负数 则停止执行的话 需明确地将无符号类型转换为有符号 这样一来 在 Swift 中混合 Int 和 UInt 的难度相较于 在 Objective-C 中混合 NSInteger 和 NSUInteger 而言就更大了 因此 规范的 Swift 的风格就是 不要这么做 而 Swift APIs 习惯使用整数 对永不为负的值也是如此
对 Apple 的框架而言 我们采用一种通用规则 即 Swift 在导入所有 NSUIntegers 时 均会将它们转换为整数
至于你自己的框架 大家可以自由选择是否更新标头 来使用 NSInteger 但我们建议这么做 这样做在 Objective-C 中的效果不明显 但在 Swift 中却能带来翻天覆地的变化
我们从更广义的角度来看 这个函数有一个更大的问题 也就是很容易会造成客户端的误用 SKRocketStageCount 函数应该与 SKRocket 常量一同使用 从它们两个名字的相同部分就能看出来 但是 Swift 却不知道这一点 因为 Swift 看到的是 这个函数需要一个字符串 这些常量都是字符串 但是其他地方也有许许多多的字符串 若你误将其他地方的字符串放到 SKRocketStageCount 中 那么肯定不会有什么好结果 在纯粹的 Swift 框架中 你可以避免这种误用的发生 只需通过一类型为字符串的原始值 将这些常量转换成一个枚举或结构体 之后修改函数 使其只接受该类型即可 你可以通过手动包装这些 APIs 对 SpaceKit 做这样的修改 但是有一个更简单的方法
首先 引入一个新的类型定义符 将常量组合在一起 之后对所有包含常量的地方进行修改 以使用类型定义符 单单这么做无法达成什么实质效果 一个类型定义符在 Swift 中 是以类型别名的形式导入的 且在两种语言中 它只是一个原始类型的同义词 但是这只是一个准备步骤
接下来在类型定义符后面添加 NS_STRING_ENUM 宏指令 这极大地改变了 Swift 中的类型定义符 现在它是作为一个结构体被导入的 内嵌套常量 使其看起来和感觉上就像一个 带有原始字符串值的枚举
最重要的是 这意味着 stagecount 函数 不再接受任意字符串 只接受 SKRocket 这样一来便大功告成了
你可以使用这一性能 来定义自己独特的字符串枚举 但是 Apple 的框架也会定义 许多字符串枚举 我列举了一些 Foundation 中常见的例子 仅在 iOS SDK 中就至少有50个 因此可以在你的 API 中 搜索一下 NSString 参数或常量 它们应该包含在这些字符串枚举中 然后将它们更新以做出匹配 接下来 让我们谈一下不正确遵守 Objective-C 的规范 而 Swift 又默认你会遵守时 会让你陷入麻烦的一些操作 在为 SKAstronaut 生成接口处 能够看到一个有趣的现象 这个类有两个初始器 这两个初始化器使用的是同一个名字 但形式却有细微的差别 一个是 PersonNameComponents 是一个 Foundation 类型 有给定姓名和姓氏的属性 另一个则是标有 “姓名” 的字符串 因此认为一个初始器可能会 调用另外一个 是比较合理的 若你将 SKAstronaut 子类化 那么 Swift 会让你覆盖掉这两个初始器 这看起来没什么必要 但是这一类的初始器还有第二个问题 这个问题无法在生成接口处看到 如果你看一下 SKAstronaut 的代码补全 则会看到没有参数的第三个初始器 这一 init 并不在生成接口中 也不在初始的标头里 感觉像是不知道是从哪里冒出来的一样 但事实上 它是从这个超类中来的 是 SKAstronaut 从 NSObject 中 承继来的 即使你的客户端可以调用它 可能运行效果也没有那么理想 这两个问题有一个相同的根本的原因 在 Objective-C 中对初始器有一个规范 确保客户端知道如何编写子类 让它永远能够正确地初始化 这一规范将初始器分为两种 指定和便捷 你需要覆盖全部的指定初始器 使得其能够安全地承继便捷初始器 如果你在想 “天啊 好像在哪里听过…”
这是因为你确实听过 Swift 类的初始器使用相同的基本模型 只有一些细节上的差别 举例来说 你在一种语言中标记了指定的 inits 在另一种语言中对便捷 inits 做标记 但 Swift 类有同样的两种初始器 它们的运行方式基本上是一样的 但不幸的是 两种语言最大的区别是 在 Objective-C 中 指定初始器不是一种语言规则 它是每个类必须选择遵守的规范 需要至少将一个初始器标记为指定 而许多 Objective-C 类 并未加入这一规范 这说明客户端不知道如何把你的类子类化 这对任何类而言都不是好事 但对框架来说尤为糟糕 因为客户端必须要读取你的源代码 或逆向工程你的行为 或纯靠猜测 而这些行为都可能导致子类出错 这本身就很令人沮丧了 但这还意味着 对于你这个框架维护者而言 在忘记覆盖一些本应被覆盖的东西时 不会收到警告 若客户端使用了你忘记覆盖的某个初始器 则意味着你的类在初始化过程中将被跳过 因此 你认为总会引用一个对象的 实例变量会变成空值
即使你覆盖掉了所有需要覆盖的内容 错误太常见了 客户端无法确定 因此 解决这一问题的第一步就是通过 在标头中标记指定初始器 来加入这个规范 若你不确定哪些初始器应当被指定 在执行端看看 通常 指定初始器会用“super”来调用一个 init 便捷初始器则用 “自身对象“来调用一个 init 因此 看一下它们的主体就知道了 一个 initWithNameComponents 应该是指定的 而 initWithNames 则是便捷的
记住了这一点 你可以返回标头文件 用 NS_DESIGNATED_INITIALIZER 对 指定初始器进行标记 而不要改动便捷初始器 在 SpaceKit 中 你需要将 initWithNameComponents 标记为指定 而不要改动 initWithName 做完这些工作后 Swift 将把 initName 识别为便捷初始器 并用一个便捷关键词来标记它
进行到这一步 可能在你的执行文件中 会开始出现需要覆盖的超类 指定初始器的警告 这是潜在的漏洞 若有人使用了其中一个漏洞 你的对象将无法被正确地初始化
若你想要自己的类支持任一上述初始器 就继续进行下去 正常地实现它们 但如果你不想的话 实施一个调用 doesNotRecognizeSelector 的覆盖 之后返回标头文件 并用 NS_UNAVAILABLE 属性宣告它 之后对超类便捷初始器做同样的操作 因为它们可能会调用已禁用的初始化器 将这些初始器标记为不可用 和不承继它们是一样的 Swift 就会自动选择不承继 若你未对指定初始器进行覆盖 通过这些调整 你的 Swift 和 Objective-C 的客户端 现在就会知道哪个初始器可以使用 哪个需要在它们的子类中覆盖了 这是共赢的方案 接下来 让我们来谈一谈 Objective-C error-handling 规范 之前 我说这个方法可能会在错误的时间被使用 我们可以从这个文件注释 或其描述的行为中 一探究竟 许多 Objective-C 的开发者 误解了这一错误处理规范 他们认为若一个方法想要发出 ”失败“ 信号 那它必须返回 ”否 “ 并将错误设置为非空值 他们认为单独返回 ”否 “ 并不是失败 只是单纯的否或空值或者其他的一些东西
但这并不是规范 规范为 若一个方法返回至一个否值 则是一个失败 即使这个错误值为 ”空“ 我们强烈建议不要把错值留在 ”空“ 因为这样的话 你的调用者无从得知发生了什么 但若你这样留着的话 一个 ”否“返回仍旧是一个失败 当 Swift 生成一个对它导入的 带有抛出的 Objective-C 方法的 调用指令 它会默认这一方法将正确地遵从这个规则 因此若方法回到 ”否 “ 它将一直抛出 Swift 并不允许抛出 ”空值“ 因此若无错误 Swfit 会抛出一个 非公共的 Foundation 错误类型 因为这一类型为非公共类型 因此你不能为其编写一个捕捉声明 但若你在日志、漏洞排除器或错误信息中 看到了这一类型和情况 那就意味着一些 Objective-C 代码 返回了 ”否“ 即使它并未失败 或失败了但未表明原因
因此让我们思考一下 如何将这一点 应用到 SpaceKit 方法上 它的说明文档显示 在跳过工作但未失败时 它能够返回 ”否“
但 Swift 默认一个否返回值 意味着它应当抛出一个错误 由于这一方法并未设置一个错误 就会成为我提到的 内部 Foundation ”空值异常“ 那我们应该如何应对呢? 有几个方案 最简单的方案是删除特例 因此否就意味着失败 并且方法是正确遵循规范的 但这样可能行不通 若客户端真的需要探测这一情况 另一个方案是使用 NS_SWIFT_NOTHROW 来告知 Swift 你并未遵从错误规范 Swift 会按照正常的方式导出 而且你可以手动写一个错误处理代码 虽然这样这个方法还是不大好用 但如果你计划弃用这个方法重写一个新的 那这个还是不错的
不管你是否将最初的保持在弃用的形式中 更好的替代方式还是要修改该方法的签名 这样才能在遵循错误处理规范的同时 用另一种方式将额外的信息返回 比如说 你可以添加一个 Boolean 值输出参数 来表明文件是否真的保存了 随后返回值就会按照错误指定的规范使用 虽然不完美 但这是单纯 Objective-C 最好的方式了 但如果你想再进一步写一写 Swift 代码 用这个方法你能得到完美的 Swift 导入 我来为大家演示
我们来看看 SKMission 的标头 大家可以看到我一直有在更新这些 现在它已经有了弃用的方法 和带着额外的参数的新方法 因为新方法遵循的是规范 太棒了 现在 让我们来写一个更好的 Swift 版 首先 我要在项目里添加一个 Swift 文件 这样可以把我的 Swift 方法放在里面 添加的方式和 Objective-C 文件一样 但选的是 Swift 的模板
我不需要把 SpaceKit 导入到这个文件里 因为它已经是框架的一部分了 SpaceKit 里不会自动显示 Objective-C 的每一部分 Swift 会自动把 SpaceKit 伞头 里面的所有东西都自动导入 伞头指的是和框架命名一样的标头 在这个框架下是 SpaceKit.h
因为伞头导入 这个框架下所有的公共标头 我的 Swift 文件能看到 这些标头里面声明的所有东西 通常来说 在框架里面有一个伞头是好的 但在使用 Swift 的框架中使用伞头 则至关重要 对于没有伞头的 app 和测试目标 Xcode 则会为这些目标 提供一个特殊的桥接标头 来实现这个功能 这里要加一句 这里有一个公共标头 我不能把它导入到伞头
这个是生成标头 这个 SpaceKit-Swift 标头声明了 我在 Swift 里标记为 @objc 的所有东西 问题是这就形成了一个循环依赖
Swift 必须先导入我伞头里面的所有东西 才能生成生成标头 所以 若伞头导入生成标头 Swift 则会试图读取 这个还没有被生成的文件 这就能把你的好心情都毁了 (构建失败) 所以 不要把生成标头导入到其他标头里 只放在你的执行文件里面
即使不是在伞头里面 启用了模块的 Objective-C 客户端 也会自动导入 所以应该没问题 回到我们当前的任务
我来扩展 SKMission
来添加一个叫保存到的新方法 两者都会抛出和返回一个布尔值 Objective-C 里 我不能返回布尔值 因为 Objective-C 错误规范 代替了返回值 但这是 Objective-C 的一个规范
在 Swift 里 返回值与该方法是否抛出错误 是完全无关的 所以这里就没有问题了 现在 我们来执行这个 首先我需要一个布尔值变量来接收脏数据
接下来 我调用 Objective-C 方法
最后 我返回这个脏数据 点击 ”构建“ 然后… 我这里出现了一个类型错误 发生了什么?
在我们的一些平台上 Swift 的布尔值 和 Objective-C 的布尔值 内存的表达稍微有一点不一样 通常来说 Swift 会插入一些转换 让运行更加平滑 但在这里 你是想要给 Swift 布尔值加一个指针 然后把指针传给 Objective-C 这样 Objective-C 就能直接读写这个值
但 Swift 在这里没办法插入转换 所以用了一个叫 ObjCBool 的类型 它和 Objective-C 布尔值的表达对应 那么 为了让这个方法成功
我需要把变量的类型改成 ObjCBool
然后在返回声明里
用它的 boolValue 属性 来返回一个 Swift 布尔值
构建 然后…
太棒了 这个方法成功了 但我还可以再优化一下
虽然我们为 Swift 客户端 准备了一个这么棒的新方法 但 Objective-C 的方法还是可用 Swift 客户端可能会弄不清楚 到底应该使用哪一个 所以最好是把它们藏起来
但我又不想 阻止 Objective-C 客户端的使用 而且我也不想 把 Swift 客户端完全屏蔽了 因为我刚刚写的那个方法还是需要用到它
那我就这样… 我去到 SKMission 标头
我给这个方法 加上 NS_REFINED_FOR_SWIFT 的标记
NS_REFINED_FOR_SWIFT 的功能非常简单 它在该方法的 Swift 名字前面 加了两个下划线 当 Xcode 看到名字开头是下划线的 Xcode 就会把它对代码补全和生成接口 这一类的编辑器功能隐藏 所以 如果我现在构建这个项目 我的 Swift 代码会报错 我们来看一看
Swift 抱怨说保存到的方法 没有一个脏参数
这说明 NS_REFINED_FOR_SWIFT 宏生效了 Swift 不觉得我试图调用的方法叫保存到 现在它觉得是叫 下划线-下划线-保存到
如果我使用代码补全
没有之前脏数据版本的任何痕迹 但即使我在代码补全时看不到这个方法 如果我在名字前加两个下划线 再构建…
这就没问题了 现在 我们的 Swift 客户端就可以用这个 特别好的包装器 调用的还是 Objective-C 方法 但这么就加上了一个单用 Objective-C 无法实现的接口 只要是能用 Swift 表达得更好的 API 你就可以用这个技巧 接下来 我们看看框架里可能遇到的问题 Swift 会竭尽全力导入标头里所有东西 但当它不知道怎么导入某个东西时 就会跳过接着导入其他的 通常这种情况发生在无法完美或自然地 将某个 Objective-C 功能 转译成 Swift 的时候 比如说 Swift 会跳过 使用 C 的可变参数和… 用声明 C 数组且大小不明的结构体成员 的功能或方法
如果我们有一个前向声明 像带有分号的 @class 或 @protocol 出现在了标头文件里 但类或协议并没有被完整声明 Swift 就没有足够的信息来导入这个类型 最后可能会跳过所有试图使用这类类型的 方法、属性甚至整个类别
如果 Swift 发现 同一个对象有两个不同的声明时 不会尝试分辨哪个是应该使用的 而是直接把两个声明都跳过 在 Xcode 12 里 Clang 现在在发现这些冲突方面进步了 如果你发现某些类型或方法 在更新时突然不见了 那就值得去查查 看是什么原因了
最后 Swift 会导入一些宏 而不是全部 这时候 SpaceKit 就会出问题了 之前 我们用 NS_STRING_ENUM 把一组字符串常量变成了一个类型 SpaceKit 其实是有两组类似的常量 我们想以同样的方式处理另外一组 但这些用宏定义的 就有一个问题 其中一个宏被丢弃了 怎么回事? Swift 不能导入你写入的所有宏
宏其实就是文本的片段 可用在 Objective-C 源代码的任何地方 同一个宏放在不同地方意思也不一样 Swift 完全无法区分宏的本来用意是什么 但符合某些常用来声明常量的模式的宏 Swift 还是可以识别的 当它看到这些常用模式的宏 就会作为一个 Swift 常量导入 如果前三个 SKCapsule 宏 符合 Swift 识别的某一模式 一个单个字符串字面量 那么 Swift 就会以常量导入 但第四个宏替代了另外一个宏 那就把字符串字面量与它连接了起来 Swift 不能完全理解宏替换 是如何其他 Objective-C 功能互动的 所以它可以允许你命名另外一个宏 或在这个宏里放其他东西 二者选一 因为 Swift 无法识别代码的模式 它就会跳过这个宏继续下一个 解决宏无法导入的方法有很多种 最简单的办法是 以同样的方式组成字符串字面量 但如果需要把这些变成字符串枚举情况 最好是把它们转换真正的常量 然后就可以把它们字符串枚举化 就像 SKRocket 常量一样 然后就完事了
好 到了这里 我们已经加强了类型 修改了一些错误的代码 确保了所有可见的内容都可见
现在就到了好玩的部分了 打磨和优化框架 让框架变得完美适配 Swift 客户端 我先从命名说起 Swift 的方法命名规范 和 Objective-C 的有些不同 两种语言使用的都是长命名 每一个参数都做了标签 但 Swift 的命名会稍微短一点 省略了从类型即可看出的明显信息
两个语言之间也有技术上的区别 Swift 中 每个方法都有一个基层名字 默认每个参数都有一个标签 Objective-C 选择器则只有参数标签 而没有单独的基层名字 这样一来 应当包含在基层名字中的信息 就被包含在了第一个参数标签里 为了帮助大家解决这些不同 Swift 在导入 Objective-C 方法时 会自动进行重命名 把对应类型名字的前缀和后缀删掉 使用英文语法和词汇表 来分清如何将选择器的第一部分 分为一个基层名字和参数标签 一般情况下结果都会很不错 但这本质上是一个做审美判断的计算程序 所以有时 程序的决定可能跟你的想法不一样 比如说 很多开发人员会觉得 这个方法选择器没有得到正确的分割 “飞” 这个词应该是参数标签的一部分 不是基层名字 因为这个方法需要获取一系列之前的任务 这些任务 都是一个特定的宇航员飞过的任务
但也不是所有开发人员都会这么想 所以看个人主观判断 要解决问题 需要把 NS_SWIFT_NAME 宏 放到方法后边 传入基层名字和参数标签 传入方法跟在 Swift 中写入的方法一样 前提是如果你想在 不调用这个方法的情况下对它进行引用
随后 Swift 会用你指定的名称 导入这个方法 而不是它自己生成的名称 但是 NS_SWIFT_NAME 不仅仅适用于方法 基本上哪里都可以用
拿这个枚举来说 Swift 已经做得很好了 因为作者用了 NS_ENUM 所以是作为 Swift 的枚举导入的 但是如果我们想微调一下名称 NS_SWIFT_NAME 也可以做到 现在 大家可以用 NS_SWIFT_NAME 来 从名称中移除 SK 前缀 但是我们不建议大家这么做 很多 Objective-C 类的名称 会把框架的前缀 和“查询”或“记录”等 单独看会非常模糊的词放在一起 必须要在名称中加一些词 才能弥补因为删除前缀而降低的精确率
针对类型的最佳处理办法是进行嵌套
比如说 这个 SKFuelKind 枚举 附属于某一个 SKFuel 类 就可以将其改为 SKFuel.Kind 基本上这就是大家在 Swift 中 对这种类型的叫法
另一个比较好的做法就是用库 这些库的类型名称 看起来与 Swift 的类型完全不一样 比如说有时候会在 C 库中看到的 小写类型名称 NS_SWIFT_NAME 也可以用于 全局常量、变量和功能 比如说 我们可以将 NS_SWIFT_NAME 用于 SKFuelKindToString 功能 既从名字里移除多余信息 也可以加一个参数标签 Objective-C 在功能上就不支持这种
但是现在我们进入了 一个非常有意思的领域 因为在适用全局常量和全局变量 特别是全局功能时 NS_SWIFT_NAME 有非常惊人的 强大的能力 能够极大地改变客户端使用框架的方式 首先 可以把全局功能变为静态方法 通过指定类型的 Objective-C 名称 名称后加一个点 然后再加上静态方法的名称就可以了
然后将其中一个参数标签改为“自身对象” 从而将其转变为实例方法 这样一来 Swift 也就知道在哪里传入 你调用的实例
随后 在前边加上“访问器” 把方法变为属性 同样 也可以用修改器 来创建一个可变属性
将这些技巧用于一整个 全都是功能的框架中 可以极大地改变整个 API 的图面 如果你在 Objective-C 和 Swift 中 都用过 Core Graphics 那你可能就明白我的意思了 我们将这个重命名能力 用在数百个全局功能上 将其转换为方法、属性和初始器 更加方便使用
现在 大家肯定会在想 有没有 NS_SWIFT_NAME 不能做的事 的确有 即便你把这个全局功能重命名 为名叫“描述”的实例属性 还是不能用 NS_SWIFT_NAME 来让类型符合 CustomStringConvertible 的协议 而这个协议会让 Swift 使用这个属性 将 SKFuel.Kinds 转换为字符串 但是你可以用一行自定义 Swift 代码 来把这个符合性加进去 那么我们来试试
我要加的一行代码是这个 我给 SKFuel.Kind 写入一个扩展
然后让它符合 CustomStringConvertible 的协议 因为我的 Objective-C 标头 已经用 NS_SWIFT_NAME 提供了一个 描述属性
这就可以了 没有下一步了 我只展示了自定义 Swift 代码 非常简单的用法 但是你可以在这些 Swift 文件中写入 任何你想要的仅限于 Swift 的 API 比如说 你可以导入 SwiftUI 并写入 SwiftUI视图 视图使用的是 你的框架中的 AppKit 或 UIKit 视图 或者你也可以选一个 使用了完成处理器之类的 API 然后写入一个 返回 Combine-Future 的包装器 我们可以 也已经放送了一整套 将这些技术与 Swift 类和类型 结合使用的视频 而如果你在一个混合语言框架中 使用这些的话 不会有什么改变 所以我们不想给大家一个填鸭式的课程 而是推荐给大家一些 SwiftUI 和 Combine 的相关视频 大家可以自行前往观看 最后 我们来讲讲错误代码的枚举 像许多框架一样 SpaceKit 需要一些 可以用在 NSError 上的个性化错误代码 为了防止其与其他框架的错误相互冲突 需要用一个错误域来声明一个字符串常量 然后用一个指定的错误代码来声明 一个NS_ENUM 大家现在看这个生成接口 可能会想 “这有什么问题?” 我们声明了一个字符串常量 这也确实是以字符串常量出现的 声明了一个 NS_ENUM 这也确实是以一个 Swift 枚举出现的 所有的条件和代码都是完整的 名称的缩写也都是对的 接口本身是没问题的
而当我们想到如何来使用的时候 问题就更清晰了 比如说你启动了一个任务 如果启动过程中断了 你会想让救援队及时救回宇航员 基本上就会是这个样子 调用 mission. launch 然后 如果是一个 launchAborted 错误 你会想要捕获这个错误 但是这个捕获子句是什么样子呢? 我们现在需要从错误中提取域和代码 所以首先我们必须要捕获 作为 NSError 的这个错误 接下来 我们需要保证这个错误是在 SKErrorDomain 中 而不是可能会因为不同原因 使用相同错误代码的 其他域
然后我们需要将错误代码从一个整数 转换为一个 SKErrorCode 最后 我们可以检查一下 这是不是我们想要的条件 看看所有的这些检查结果 这不是… 我希望我的客户端写入的东西
不过是要找一个错误 不应该花这么大力气
如果你在 Swift 中写入了 SpaceKit 而这之前是一个 Swift 错误枚举 你可以用不到一行就达到一样的目的 只要命名错误类型以及条件 Swift 会将其与抛出的错误匹配 这样就会好很多 但当然了 你没有 Swift 错误枚举 你有的是一个 Objective-C 枚举 和一个错误域常量
那么你可以将这些转换为 类似 Swift 错误枚举的东西吗? 可以 而且方法非常简单 你所要做的 就是将 NS_ENUM 替换为 NS_ERROR_ENUM 然后将原始类型替换为错误域常量
这对错误代码枚举有非常大的效果
SKErrorCode 可以 嵌套在 SKError 当中 也就是一个凭空发明的新类型 Swift
在 Xcode 生成接口中 可以看到枚举中有 作为条件的所有错误代码 而且在结构体上作为静态常量 也重复出现了 而且还有一个域的静态常量
但是 SKError 也符合错误 所以就可以像对待原生 Swift 错误一样 进行抛出和捕获
它有针对错误代码和客户端信息字典的 初始器和属性 且错误代码枚举有一个 波浪号-等号操作符 这是条件和捕获子句使用的匹配操作符 这里写道 如果要切换或捕获错误 可以与 SKErrorCode 相匹配 只有错误域和错误代码都正确的情况下 才能成功匹配 因为 SKError 上有静态代码常量 可以写入 catchSKError.launchAborted Swift 会用这个操作符 来将错误与错误代码匹配 这部分的 SKError 不会在生成的接口中 显示出来 因为 Swift 编译器对其进行了同步 但是你可以用 而且还会让 Objective-C 错误代码 在 Swift 中运转得非常不错 你要做的唯一一件事就是更改两个标识符 对一行的差异来说 很不错了
但是我想回到错误代码的最初步骤 想想有没有更重要的经验值得学习 在生成接口中没有明显的错误迹象
只有在看到 SKErrorCode 的用法时 才会意识到这里需要改进 在这期视频中 我一直在给大家看生成接口 因为这样大家可以更好地看到 Swift 是如何导入你的标头的 但是生成接口 只是一个了解框架内容的工具 真正重要的是你的客户端在使用时 会写入的调用 那么当你在建立框架时 应该看看生成接口 但也应该想想使用站点 从你的视角来想像一下 在白板上做个笔记 在游乐场中做一下修补 在测试中进行编码 你的客户端会根据在框架上写入的代码 来决定对你的框架的喜恶 而不是根据 他们在命令单击时看到的接口决定
那么总结一下
如果你有 Objective-C 框架 可以让其为 Swift 客户端 提供良好的服务 所要使用的不过是标头注解 有时会用到一点点 Swift 代码
在做这些工作的时候 找找有没有机会 来向 Swift 客户端提供更强大 和更具体的类型
并保证遵守 Objective-C 的规范 这样 Swift 才会正确地使用你的框架
虽然 Xcode 给你展示了生成接口 但是要看得远一些 想想你的 API 是如何使用的 这才应该是推动设计的动力
如果想要了解更多信息 Swift 文档中有一个叫做 ”语言互操作性“的部分 深入分析了所有的这些功能以及其他内容 如果想要更具体地了解 如何让 Swift API 符合语言习惯 ”API 设计指南“也说明了 Swift 核心团队和社区推荐的 部分原则和规律 ”Xcode 编译流程的幕后“这个视频 深入讲解了 Swift 如何导入 Objective-C 标头 以及 Xcode 如何编译混合框架
如果 Swift 缺少了部分 Objective-C 框架标头 或者根本就没导入 这个视频就非常有用了
谢谢大家观看本期视频 希望本期视频能够 帮助您开发出优秀的产品
-
-
4:43 - Describe nullability to control optionals (method and property annotations)
// // SKMission.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> @interface SKMission : NSObject @property (readonly, nullable) NSString *name; - (nonnull instancetype)initWithName:(nullable NSString *)name; @end
-
6:53 - Describe nullability to control optionals (ASSUME_NONNULL blocks)
// // SKMission.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface SKMission : NSObject @property (readonly, nullable) NSString *name; - (instancetype)initWithName:(nullable NSString *)name; @end NS_ASSUME_NONNULL_END
-
7:14 - Describe nullability to control optionals (qualifiers)
// // Misc.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> NSString * _Nonnull const SKRocketSaturnV; @interface ResourceValueContainer : NSObject - (BOOL)getResourceValue:(id _Nullable * _Nonnull)outValue error:(NSError**)error; @end
-
8:09 - Finding nullability mistakes with Objective-C tools
// // SKMission.h // #import <Foundation/Foundation.h> @interface SKMission : NSObject @property (strong, nonnull) NSString *rocket; @property (strong, nonnull) NSString *capsule; @end // // SKRocket.h // #import <Foundation/Foundation.h> extern NSString *_Nonnull const SKRocketSaturnV; // // SKMission.m // // Try building this file and then try analyzing it. // #import "SKRocket.h" #import "SKMission.h" @implementation SKMission @end @interface SKMissionConfigurator : NSObject @property (strong, nullable) SKMission *mission; @end @implementation SKMissionConfigurator - (void)testBadUseWithWarning { [self.mission setCapsule:nil]; } - (void)testBadUseWithStaticAnalyzer:(BOOL)missionIsSkylab1 { NSString *capsule = nil; if (!missionIsSkylab1) { capsule = SKCapsuleApolloCSM; } self.mission.capsule = capsule; } @end
-
11:07 - Use Objective-C generics for Foundation types
// // SKAstronaut.h // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface SKAstronaut : NSObject // Stub declaration @end NS_ASSUME_NONNULL_END // // SKMission.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> #import <SpaceKit/SKAstronaut.h> NS_ASSUME_NONNULL_BEGIN @interface SKMission : NSObject @property (readonly) NSArray<SKAstronaut *> *crew; @end NS_ASSUME_NONNULL_END
-
11:33 - Use Int for numbers—unsigned types are for bitwise operations
// // SKRocket.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN NSInteger SKRocketStageCount(NSString *); NS_ASSUME_NONNULL_END // // NSData+xor.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> @interface NSData (xor) - (void)xorWithByte:(uint8_t)value; @end
-
13:23 - Strengthen stringly-typed constants
// // SKRocket.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN typedef NSString *SKRocket NS_STRING_ENUM; extern SKRocket const SKRocketAtlas; extern SKRocket const SKRocketTitanII; extern SKRocket const SKRocketSaturnIB; extern SKRocket const SKRocketSaturnV; NSInteger SKRocketStageCount(SKRocket); NS_ASSUME_NONNULL_END
-
15:24 - Specify initializer behavior
// // SKAstronaut.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface SKAstronaut : NSObject - (instancetype)initWithNameComponents:(NSPersonNameComponents *)name NS_DESIGNATED_INITIALIZER; - (instancetype)initWithName:(NSString *)name; - (instancetype)init NS_UNAVAILABLE; @property (strong, readwrite) NSPersonNameComponents *nameComponents; @property (readonly) NSString *name; @end NS_ASSUME_NONNULL_END // // SKAstronaut.m // #import "SKAstronaut.h" @interface SKAstronaut () @property (class, readonly, strong) NSPersonNameComponentsFormatter *nameFormatter; @end @implementation SKAstronaut - (id)initWithNameComponents:(NSPersonNameComponents *)name { self = [super init]; if (self) { _name = name; } return self; } - (id)initWithName:(NSString *)name { return [self initWithNameComponents:[SKAstronaut _componentsFromName:name]]; } - (id)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (NSString *)name { return [SKAstronaut.nameFormatter stringFromPersonNameComponents:self.nameComponents]; } + (NSPersonNameComponents*)_componentsFromName:(NSString*)name { return [self.nameFormatter personNameComponentsFromString:name]; } + (NSPersonNameComponentsFormatter *)nameFormatter { static NSPersonNameComponentsFormatter *singleton; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ singleton = [NSPersonNameComponentsFormatter new]; }); return singleton; } @end
-
20:00 - Follow the error handling convention
// // SKMission.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface SKMission : NSObject /// \returns \c YES if saved; \c NO with non-nil \c *error if failed to save; /// \c NO with nil \c *error` if nothing needed to be saved. - (BOOL)saveToURL:(NSURL *)url error:(NSError **)error NS_SWIFT_NOTHROW DEPRECATED_ATTRIBUTE; /// @param[out] wasDirty If provided, set to \c YES if the file needed to be /// saved or \c NO if there weren’t any changes to save. - (BOOL)saveToURL:(NSURL *)url wasDirty:(nullable BOOL *)wasDirty error:(NSError **)error; @end NS_ASSUME_NONNULL_END
-
22:40 - Refine an Objective-C API for Swift users
// // SKMission.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface SKMission : NSObject /// \returns \c YES if saved; \c NO with non-nil \c *error if failed to save; /// \c NO with nil \c *error` if nothing needed to be saved. - (BOOL)saveToURL:(NSURL *)url error:(NSError **)error NS_SWIFT_NOTHROW DEPRECATED_ATTRIBUTE; /// @param[out] wasDirty If provided, set to \c YES if the file needed to be /// saved or \c NO if there weren’t any changes to save. - (BOOL)saveToURL:(NSURL *)url wasDirty:(nullable BOOL *)wasDirty error:(NSError **)error NS_REFINED_FOR_SWIFT; @end NS_ASSUME_NONNULL_END // // SwiftExtensions.swift // import Foundation extension SKMission { public func save(to url: URL) throws -> Bool { var wasDirty: ObjCBool = false try self.__save(to: url, wasDirty: &wasDirty) return wasDirty.boolValue } }
-
31:35 - Fix method names with NS_SWIFT_NAME
// // SKMission.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> #import <SKAstronaut/SKAstronaut.h> NS_ASSUME_NONNULL_BEGIN @interface SKMission : NSObject - (NSSet<SKMission *> *)previousMissionsFlownByAstronaut:(SKAstronaut *)astronaut NS_SWIFT_NAME(previousMissions(flownBy:)); @end
-
33:12 - Rename and rework value types with NS_SWIFT_NAME
// // SKFuelKind.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface SKFuel : NSObject // Stub class @end typedef NS_ENUM(NSInteger, SKFuelKind) { SKFuelKindH2 = 0, SKFuelKindCH4 = 1, SKFuelKindC12H26 = 2 } NS_SWIFT_NAME(SKFuel.Kind); NSString *SKFuelKindToNSString(SKFuelKind kind) NS_SWIFT_NAME(getter:SKFuelKind.description(self:));
-
35:59 - Add conformances to Objective-C types using custom Swift code
extension SKFuel.Kind: CustomStringConvertible {}
-
37:02 - Improve error code enums
// // SKError.h // SpaceKit // #import <Foundation/Foundation.h> extern NSString *const SKErrorDomain; typedef NS_ERROR_ENUM(SKErrorDomain, SKErrorCode) { SKErrorLaunchAborted = 1, SKErrorLaunchOutOfRange, SKErrorRapidUnscheduledDisassembly, SKErrorNotGoingToSpaceToday };
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。