前段时间发现了一个十分强大的工具:Sourcery,它很好的解决了我在Swift开发中遇到的一些问题,在中文社区中sourcery彷佛并非颇有名,因此这里特意写一篇文章来做介绍。本文大体分为三个部分:git
不少人可能对元编程(meta-programming)这个概念比较陌生,固然有一部分是由于翻译的问题,这个“元”字看起来实在是云里雾里。若是用一句话来解释,所谓元编程就是用代码来生成代码。程序员
这句话能够从两个层面上来理解:github
OC有着十分强大的Runtime特性,在运行时能够查看和修改一个对象的全部成员,因此有了Mantle
之类JSON转Model的库;甚至能够在运行时添加、删除、替换一个类型中的方法,固然也能够动态的添加类型,因此有了Aspects
和AOP
。这些应用均可以概括为元编程的范畴,由于它们的功能都是经过在运行时修改程序自己来实现的,这一特性为咱们节省了不少重复的样板代码。shell
而Swift是一门静态强类型语言,没有OC这样强大的运行时特性,虽然Swift也能够接入OC Runtime,可是那很容易让你的代码变成“用Swift写的OC”,并且对运行时的修改容易让程序变得难以理解。既然这样,再来看看Swift自身的反射机制,Swift提供了一个名为Mirror
的类型用来在运行时检查对象的属性,可是一方面Mirror
只能查看不能修改,另外一方面它的性能不好,文档中也建议仅在Debug的时候使用。编程
因此说第一条路子在Swift中是走不通了,只能从另外一个方面来寻找答案,所幸的是已经有了一套成熟的解决方案,那就是下面要介绍的Sourcery。swift
简单来讲Sourcery
是一个Swift代码的生成器,它可以根据咱们预先定义好的模板来自动生成Swift代码。app
以官方的Demo为例,好比说你有一个自定义的类型:工具
struct Person {
var name: String
var age: Int
}
复制代码
想要为这个类型实现Equatable
协议,必须在==
方法中依次比较每个属性的相等性:性能
extension Person {
static func ==(lhs: Person, rhs: Person) -> Bool {
guard lhs.name == rhs.name else { return false }
guard lhs.age == rhs.age else { return false }
return true
}
}
复制代码
一般咱们的项目中都会有大量的Model类型,若是要为它们都实现Equatable
,会带来大量重复的工做。并且若是你在一个类型中添加了新的属性的话,必须同步修改它的Equatable
实现,不然可能会出现难以预料的Bug。ui
Sourcery能够将咱们从这些繁琐的样板代码中解放出来,首先咱们须要为全部的Equatable
实现定义一个统一的模板,这部分是经过一门名为Stencil的语言来编写的。Stencil
是一门专门为Swift设计的模板语言,语法十分简单,对于上面代码能够定义这样的模板(模板的编写推荐使用vscode加上stencil插件):
{% for type in types.implementing.AutoEquatable %}
extension {{type.name}}: Equatable {
static func ==(lhs: {{type.name}}, rhs: {{type.name}}) -> Bool {
{% for variable in type.storedVariables %}
guard lhs.{{variable.name}} == rhs.{{variable.name}} else { return false }
{% endfor %}
return true
}
}
{% endfor %}
复制代码
代码中出现的AutoEquatable
是预先定义在咱们本身代码中的一个协议,只是一个做为标记用的空协议:
protocol AutoEquatable { }
复制代码
它的做用是让咱们可以在模板中找到须要的类型,只需将自定义的Person
类型声明为实现AutoEquatable
,以后在模板中就能够经过types.implementing.AutoEquatable
找到目标类型,而后经过type.storedVariables
来遍历类型中的全部储存属性生成对应的比较代码。
定义了模板以后就能够经过这个模板来生成代码了,首先在系统中安装Sourcery:brew install sourcery
。以后运行下面的指令:
sourcery \
--sources ./YourProject \
--templates ./YourTemplates \
--output ./YourProject/AutoGenerated.swift
复制代码
其中--source
指定了工程的根目录,--templates
指定存放模板文件的目录,--output
将生成的代码输出到指定路径,除了命令行也能够经过一个.sourcery.yml
文件来定制参数,这里就再也不展开介绍了。
以后就能在工程的路径下看到一个名为AutoGenerated.swift
的代码文件,它包含了这样的内容:
// Generated using Sourcery 0.12.0 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
extension Person {
static func ==(lhs: Person, rhs: Person) -> Bool {
guard lhs.name == rhs.name else { return false }
guard lhs.age == rhs.age else { return false }
return true
}
}
复制代码
生成的代码文件是须要参与编译的,记得将它添加到工程中。
接着,咱们能够将代码生成这一步整合到Xcode的编译流程中,在Build Phases
添加这样一个脚本(这里我把sourcery二进制文件也加到了工程目录中):
须要注意的是这个脚本必定要添加在Compile Sources
以前,不然新生成的代码没法参与编译。在这以后只要咱们的类型实现了AutoEquatable
,不管是添加仍是删除属性,每次Build代码就会自动更新,免去了手动修改的困扰。
以上的Equatable
只是做为示例,完整的版本请看官方提供的这个模板AutoEquatable.stencil
从上面的例子中能够看出来,Sourcery之因此如此强大,关键在于模板解析时可以获取咱们代码中的全部类型信息,这使咱们在编写模板的时候得到了极大的自由度。Sourcery使用了两个关键的技术来实现这一切:Stencil和SourceKitten。
在以前的介绍中也提到了,Stencil
是一门用Swift实现的专门为Swift设计的模板语言,它的语法十分简单,只解析下面这三种语法模式:
{{ ... }}
:变量语法,将中间的部分做为变量(或变量的表达式)来解析,解析后的值会做为结果插入到模板中的相应位置上。{% ... %}
:标签语法(Tag),标签用来表示一些具备特殊功能的语法,好比用来实现判断的if
和循环的for
。{# ... #}
:注释语法,不会出如今解析后的结果中。除此以外还有一个名为Filter
的概念,它的语法是这样的:{{ "stencil"|uppercase }}
。符号|
左边是输入的变量,右边就是一个Filter
,这里输出了字符串的大写形式。Filter
本质上是一个输入和输出都是Any
的方法,好比说上面的uppercase
在源码中对应是这样的:
registerFilter("uppercase", filter: uppercase) // 注入一个Filter
func uppercase(_ value: Any?) -> Any? {
return stringify(value).uppercased()
}
复制代码
一样模板解析时能够访问的变量也是在运行时注入到Stencil
环境中的。Stencil
有着十分强大的扩展性,github上有一个这样的库StencilSwiftKit,为Stencil
扩展了许多更加便捷的语法。
Xcode对Swift和OC的处理有一点不一样的地方,OC的编译器是在Xcode进程中执行的,而Swift的编译器是在一个独立的进程中进行的,所涉及到的一系列编译工具的集合称为SourceKit
,编译的结果经过XPC与Xcode进行通讯。
这样一来就有机会对编译中间的结果作一些分析,SourceKitten就是这样一个开源库,它与SourceKit进行交互并将代码的语法结构转换成JSON的形式返回。利用SourceKitten,Sourcery能够获取代码中全部类型的相关信息,并将它们做为变量注入到了Stencil
的上下文环境中,因此咱们才能在模板中用{{ types }}
这样的方式遍历代码中的全部类型。
下面所介绍的Demo已上传至个人Github:AutoCodableDemo。
Codable
是Swift4引入的对JSON解析的原生支持,与ObjectMapper
之类的第三方库相比,它能够自动地解析Model中的属性,若是你的数据模型和JSON结构彻底一致的话,使用起来将会很是简单。
然而现实每每并非这么美好,不少时候须要对解析作一些自定义,这样一来操做将会变得十分繁琐,要自定义KeyPath首先得为类型定义一个实现了CodingKey
的枚举,这个枚举中要包含全部的属性字段,即便这个属性不须要自定义;而若是要作更加复杂的自定义的话还得本身实现init(from decoder: Decoder)
和encode(to encoder: Encoder)
方法,并为全部的属性实现decode和encode操做。
显然这些代码具备很高的重复性,很是适合使用Sourcery来自动生成:
首先在项目中定义一个AutoCodable
类型:
protocol AutoCodable: Codable { }
复制代码
在模板中找到全部实现了AutoCodable
的类型,并在扩展中为它们自动加上一个包含了全部属性名的枚举:
enum CodingKeys: String, CodingKey {
{% for var in type.storedVariables %}
case {{var.name}} {% if var|annotated:"key" %}= "{{var.annotations.key}}"{% endif %}
{% endfor %}
}
复制代码
Sourcery提供了一个名为annotation
的机制,能够在代码中以注释的形式向模板提供一些必要的数据,只须要在某个变量或是类型的定义前加上一行这样的注释:
// sourcery: key = "value"
var something: Int
复制代码
Sourcery会将这种格式的注释解析出来,以key-value
的方式添加到模板中该变量所对应的annotations
属性上,经过这种方式能够在代码中为模板解析提供一些自定义的数据。
让你的自定义类型实现AutoCodable
:
struct Person: AutoCodable {
var myName: String
}
复制代码
AutoCodable
实现了如下功能:
自定义字段名称: 在须要自定义字段名称的属性前加上这样一个annotation
:
// sourcery: key = "my_name"
var myName: String
复制代码
设置属性默认值: AutoCodable
容许你为属性提供默认值,当JSON中的该字段解析失败时该属性会被设置为默认值,而不是抛出错误,有了默认值以后该属性再也不须要定义成可选类型:
// sourcery: default = true
var something: Bool
复制代码
忽略某个字段: 被忽略的属性不会参与JSON的Encode和Decode,另外被忽略的属性必须带有一个默认值:
// sourcery: skip
var something: Int = 0
复制代码
支持将Int解析成Bool类型:
Codable
在解析JSON的时候对于类型是有严格要求的,若是一个属性的类型是Bool
,在JSON中对应的字段值是Int
类型的话会抛出一个类型错误(不像OC中的Mantle会自动转换)。 虽然Codable
的这个作法无可厚非,然而在咱们的实际项目中已经有大量的后台接口数据使用1和0来表示true和false了。因此在这里AutoCodable
针对Bool
类型作了处理,支持将Int
类型的值解析成Bool
类型。
以后像上面所介绍的那样将生成的代码文件添加到工程里便可,能够看到Sourcery为咱们免去了自定义解析时大量重复的代码,惟一的缺点就是向模板传值只能经过注释的形式,在Xcode添加一个Code Snippet:// sourcery: <#key#> = <#value#>
能提供一些帮助,至于Key的名称就只能在编码的时候注意别写错了。除此以外Sourcery已经完美的解决了我在使用Codable
时碰到的问题。
Sourcery本质上至关于一个预处理器,它为Swift带来了灵活的元编程特性,你甚至能够将生成的代码内嵌到本身的代码中,它的应用场景远远不仅是上面所介绍的这些。程序员的时间是宝贵的,咱们应该将精力集中在真正关键的部分,若是你也在使用Swift,不妨来尝试一下,和那些琐碎重复的样板代码挥手道别😄。