Swift 中的元编程
在开发 iOS 中, 我们有很多的模板代码, 很重复, 但是又不得不写的. 这时候, 如果能自动按我们的需求自动生成对应的代码, 那就再好不过了, 从维护一堆相似的代码, 到维护一个模板.
元编程的提出
就 Swift 来说, 在编译器级别, 已经有了可以拿到项目中全部代码的工具, swiftlint
, 以前 Xcode 上的插件等, 都是基于这类方法做的.
这里就介绍一个工具, 通过代码来生成代码. —- [Sourcery](https://github.com/krzysztofzablocki/Sourcery)
.
Sourcery 是一个 Swift 代码生成的开源命令行工具,它 (通过 SourceKitten) 使用 Apple 的 SourceKit 框架,来分析你的源码中的各种声明和标注
其实在其 github 主页, 已经有了很明确的它是什么, 以及怎么使用的说明. 这里还是强烈建议下阅读里面推荐的这篇入门教程, 还有他的一个讲这个的演讲, 比较生动.
其实每个文章, 都已经举了简单的例子来说明这个工具的使用方法, 如果你看了之后还是会有所困惑, 那是因为你没有切实的用起来. 当你用起来后, 自己经历过就会感觉他的强大! 省了自己多少时间!
里面很多例子都用了 Equatable
来举例
在使用struct
时, 想要比较两个对象相等, 需要遵守这个协议, 然后实现==
函数. 如
1 |
|
代码看着少? 试想一下这个结构体里有十几个属性, 然后你有十几个这类型的结构体. 这就是一场屠杀…
这时候, Sourcery 就是救星…
Sourcery 能自动根据一些规则, 使用定义的模板来生成你想要的代码. 使用它, 你能获取整个项目中的某些数据, 这里用的最多的, 应该是你做了标注的数据, 这样将变化控制在自己的范围里. 这个标注后面会说到.
模板
这些模板可以使用几种语言来编写, 官方用的和比较推荐的是, 比较简单轻量的 [Stencil](https://stencil.fuller.li/en/latest/)
你甚至可以使用 Swift 来写模板, 实现更丰富复杂的逻辑, 但是这样的话, 也许应该考虑下项目的结构是不是有什么问题. 因为模板, 主要是用来生成一些模板代码的, 不是用来写逻辑的.
其实 Stencil 是弱化版的 Swift, 可以看到在语法上是基于 Swift 的, 但是有一些自己的特性
使用模板和 Sourcery 来生成上面那样的模板代码:
首先你需要写一个模板, 即你想生成的代码是什么样的, 其中的关键点已经在代码中注释出来
1 | // 这里是最重要的一行, 在这里, 我们遍历了项目中每个实现了`AutoEquatable` 协议的类型(type), 这些协议是我们定义的, 仅仅是作为一个 maeker |
以上, 我们就为项目中所有实现了AutoEquatable
的类, 加了一个实现Equatable
的extension
, 并且实现了要求的方法==
, 在方法中比较了每个存储属性的值是否相等.(计算属性的不必比较)
我们来动手一下~
首先你需要有 Sourcery, 这个在其主页有安装方法, 现在假设你 已经安装好了.
按这篇文中的假设是, 他装到了项目的./Resources/sourcery
这个位置, 目录是和***.xcodeproj
同级目录
当然, 这个也可以使用brew install sourcery
来安装.
现在, 运行这段代码:1
2
3
4./Resources/sourcery/bin/sourcery
--sources ./SourceryDemo
--templates ./SourceryDemo/Templates/
--output ./SourceryDemo/Autogenerated
我们一半监控项目的目录, 即和***.xcodeproj 同级的和项目同名的那个目录
这时, 你可以在输出文件中看到效果, 它生成了我们想要的代码!
自动重新生成
试想一下, 如果给某个类加了一些存储属性, 这时候, 生成的那些代码没有变, 你需要手动再运行下那个代码, 重新生成这些模板代码.
但是这样很烦啊有木有…
最好的方式就是我们改了模板, 对应的地方的代码会自动根据修改的模板自动做出改变.
Sourcery 给我们提供了这种方式, 就是在我们的命令中加上 --watch
这个选项, 它就会一直监听你的模板文件和对应的输出文件.
可以试试, 在保存模板文件的时候, 能实时的看到输出文件的改变, 这时候, 你用一个可以同时打开两个文件的编辑器, 就能看到这种效果, 很爽是把~
使用 VSCode 的话, 可以通过这个操作, 打开
Stencil
的语法高亮
It’s worth noting that Visual Studio Code doesn’t ship with Stencil highlighting by default. To install it, press ⌘-T and then enter the command ext install stencil.
注意: 这里的监控仅仅是监控了这条命令执行时的模板文件和输出文件
Pre-Build 执行
一旦你有了一堆的模板, 使用这种方式去维护项目中的模板代码, 你一定不想再去手写任何一个模板代码了…
注意, 上面的一条说到, 监控的只是输入文件和输出文件, 并没有说监控整个项目的变化, 所以, 当你有一个新的类遵守了某个marker protocol (自动生成摩拜代码的协议)
时, Sourcery 并不会自动就给你创建代码, 你需要重新运行, 他才会从项目中拿到对应的所有类型并生成代码.
我们怎么保证没次我们的更改总是能够让 Sourcery 来生成对应的代码? 我们可以在 Xcode添加一个 pre-build step
, 在 Xcode 中选中你的target
-> Build Phase
-> Run Script
添加一个新的脚本, 把上面运行的命令添加到里面.
这时候, 当我们没次 build 项目的时候, 这里的第一步都会刷新 Sourcery 的输出.
练习例子
这个文的博主还贴了他的一个做这个尝试的sample, 可以根据 git 的历史来一步步的跟着做, 他还有一篇文是讲 RxSwift 的, 这也很有用, 也有一步步跟着做的 sample~
ref: Soucery 的魔法
更多的姿势
上面讲了什么是Sourcery
, 以及他在一个完整项目中的应用和基本使用方法. 这里接下来讲讲他更多的使用姿势.
当然, 看 Sourcery 文档 是最好的最准确最全面的. 但是个人觉得, 需要解答疑惑时, 看文档才是最好的时机, 因其大而全, 先看了文档再去动手会错失很多时机, 反而看看别人介绍的怎么用, 用的最多的功能和常用的方法是开始入手一件事最好的方式~
在运营Sourcery
时, 你可以在命令行指定他的一些参数, 但是, 本人清冽认同参考帖子的作者, 使用配置文件将会使的配置更灵活, 更好维护, 而且能备份和分发.
Sourcery
支持使用配置文件, 他使用的配置文件使用YAML
, 一种很适合配置文件的语言.
里面需要声明
sources
文件的位置templates
文件的位置output
输出的位置
这个配置文件我们需要放在和urProjName.xcodeproj
同级的目录下.
我们在这里创建一个文件, 叫: .sourcery.yml
(注意这种命名是隐藏文件), 然后打开它, 将下列内容粘贴进去
1 | sources: |
然后你需要创建一个叫Templates
的模板文件夹, 注意还是同级目录.
插播一个..令人鸡冻的消息,
Swift4.2
介绍了一个针对 Enum 的协议:CaseIterable
, 在编译时自动synthesizes
一个数组, 里面包含了所有的case
, 使用allCases
调用, 我擦, 这不是之前很烦的, 需要自己写的一个东西吗?! 斯巴拉西, 这届的Swift
真是我带过的最好的一届!
但是…如果你还没有使用到4.2的话, 当你调用
.allCases
的话会爆一个compile error
, 你必须要手写这个方法, 或者用Sourcery
生成.
Sourcery
和 Stencil
的结合, 给我们带来一些内建的值来编写模板.
我们可以通过types.enums.implementing.CaseIterable
来获取实现相应协议的枚举
like this
1 | protocol CaseIterable { } |
在***.xcodeproj
同级目录的终端下, 运行 sourcery --watch
, 然后Soucery
就会在当前目录找到配置文件, 根据配置文件来启动服务. 然后根据模板文件生成代码到输出目录, 命名类似这个: CaseIterable.generated.swift
, 类似这种全生成的文件不能手动去改变里面的内容, 因为每次修改模板的时候这里都会被重写.
当然, 你需要把这个目录拖到 xcode 项目中, 他需要和其他你项目中的代码一起起作用.
当你代码改变的时候, 这个文件也会相应的改变(如果监控的 sources 文件有变化的话. 然后生成的文件中. 会有遵守这个协议的枚举的扩展, 里面实现了一个属性allCases
, 里面是一个数组包含了所有的case
但是这个在 Swift4.2 中会报错的, 因为已经有了这个方法了.
我们可以在代码中使用诸如下列注释来告诉编译器一些条件:1
2
3
4
5// 里面没有<的说法
#if swift(>=4.2)
// do thing here
#else
#endif
我们可以在模板中加入这里, 使得在生成的代码中包含这个编译器判断条件
还有一个问题, 这个获取到的所有的case
, 也包含了一些值绑定的 case, 这个就不对了. 我们可以在模板语言中加入一句判断来排除掉这种 case
1 | `{% if not enum.hasAssociatedValues %}` 在循环中加入这个判断就能排除不需要的类型. |
还有这个模板, 会在最后一个 case 后面依然加入了一个逗号, 我们并不想这么做, 虽然不要紧. 但是就是为了好看, 不想这么做, 颜值即正义…
而且这样, 可以引申出一个 Sourcery 一个有用的功能: 在循环里面, 可以通过forloop.firs
, forloop.last
, forloop.counter
, and forloop.counter0
来判断是否第一个或最后一个, 或者拿到基于0或者基于1的当前 item 的下标. 这个例子里是获取是否是最后一个 item, 不是的话就加上逗号.
1 | {% for case in enum.cases %} .{{ case.name }}{% if not forloop.last %},{% endif %} |
最后, 我们的模板是这样的:
1 | #if swift(>=4.2) |
题外话
gyb
代码生成方式的另一个“流行”选择时 gyb (Generate Your Boilerplate)。gyb 严格来说就是一个 Python 脚本,它将预定义的值填充到模板中。这个工具被大量用于 Swift 项目本身的开发,标准库中有不少以 .gyb 作为后缀的文件,比如 Array 就是通过 gyb 生成的。
gyb 设计的最初目的主要是为了解决像是 Int8,Int16,Int32 等这一系列十分类似但又必须加以区分的类型中模板代码问题的。(鉴于 Apple 自己都有可能用其他工具来替换掉它,) 我们这里就不展开介绍了。如果你对 gyb 感兴趣,可以看看这篇简明教程。
然后后面说到那些也没时间去了解了, 这里就不搬运了. 有兴趣可以自己去看看