为何造这个轮子
国庆的时候写了个小工具来将 JSON 转化成 Model,也算是我的第一个 Mac App,今天重构了下,顺便跟大家分享下 Mac 开发有多不方便……
项目地址在这里,如果是跟我一样使用 ObjectMapper 的朋友可以直接下载使用一下。市面上其实已经有一些 JSON 转 Model 的工具了,那我为何还要重复造轮子呢?显然是因为别人写的并不能满足我的需求,代码生成器绝对是个需要定制化的东西,毕竟每个人每个团队,都有一套代码风格(不单是指缩进、空格、大括号换不换行这些,因为这些其实每个社区几乎都有广为人接受的最佳实践),比如我写 Model 不喜欢把 String、Int 等类型的属性声明为 Optional,而是习惯给它们一个初始值,但是对象类型的属性给它个初始值我又觉得开销有点大,一般就用 Optional。所以我写的这个工具也不一定适合你们,但是我把我的一点微小经验分享给大家,你们就可以随意修改我的代码进行定制化或者自己重新造个最趁手的轮子。当然,如果你确实有需要,而自己又没时间造轮子,可以留言告诉我,我会考虑扩展功能。
界面
好了话不多说先放张截图,图中的 JSON 数据来自 GitHub API 文档:
UI 非常简单,左边用来输入 Model Name 和粘贴 JSON,右边是转化结果。我对 Mac 开发其实一无所知……我就是直接打开 Xcode,新建了个 macOS 的项目,然后在 Storyboard 上拖了一个 TextField 和两个 TextView 进去,设置好约束之后,我准备把三个控件连到代码中……然后我尴尬地发现 NSTextView 连到 IBOutlet 之后,类型是 NSScrollView [黑人问号❓❓❓]。这个时候我还没有意识到自己已经一只脚踏进坑里,我心想难道 NSTextView 是 NSScrollView 的子类?那我手动把它改成 NSTextView 吧……然后迎接我的是各种 crash。后来仔细揣摩了一下 Storyboard 里控件的层级关系,我发现之前真的只是单纯地连了个 NSScrollView 到代码中,真正的 NSTextView 在一个奇怪的地方:
我之前拖到代码中的是最外层的 Bordered Scroll View,它下面还有一层 Clip View,之后才是我需要的 TextView……
思路分析
有人可能觉得代码生成器是个很高端很难实现的东西,其实不然。代码生成器的难点在于解析输入,而输入的规则很多情况下是我们自己定的,只要尽可能保证解析规则简单,剩下的工作就是把解析好的信息填到预定的模版中输出而已。拿我的 Model 生成器来说,最难的任务本应该是解析 JSON 字符串,但是我直接把字符串序列化然后生成结构化的 JSON 数据,这一步就只需要两行代码:
func json(from text: String) -> Any? {
guard let data = text.data(using: .utf8) else { return nil }
return try? JSONSerialization.jsonObject(with: data, options: [])
}
JSON 对象有了,我们还需要把属性名进行标准化。Swift 中变量名是使用驼峰风格的,如果你的服务端是用 PHP,Ruby 之类的语言写的,返回的 JSON 中的 key 一般是用下划线分隔单词的,我们可以这么做:
func normalizeVariableName(key: String) -> String {
var name = key
if name.contains(underline) {
var words = name.components(separatedBy: underline)
name = words.removeFirst()
words.forEach { name += $0.capitalized }
}
return name
}
接下来就是边解析 JSON 边拼接字符串,难点在于嵌套对象的处理,我使用了递归,代码有点长我就不贴了,大家可以看源码。还有就是碰到了对象数组的话,对于该对象 Model 的命名,也不太好办,我的处理是属性名以“s”或者“List”结尾的话,就把“s”或者“List”之前的单词作为 Model 名,至于其它的情况(譬如 people、productArray 等),就管不过来了,真的碰到了就手动修改下。按《程序员修炼之道》中说的:
这是被动代码生成器的一个有趣的特性:它们不必完全正确。你需要在你投入生成器的努力和你花在修正其输出上的精力之间进行权衡。
在 Mac 开发中使用 RxSwift
感觉 RxCocoa 对 Cocoa 的支持并不好,譬如没有为 NSTextView 和 NSTextField 提供 rx.string、rx.stringValue 之类的扩展。不过关系也不大,可以自己用 PublishSubject 去接一下相应的委托方法,然后我们只要订阅这个 PublishSubject 就好了:
extension ViewController: NSTextViewDelegate {
func textDidChange(_ notification: Notification) {
source.onNext(sourceText.string ?? "")
}
}
extension ViewController: NSTextFieldDelegate {
override func controlTextDidChange(_ obj: Notification) {
let model = modelNameText.stringValue.isEmpty ? "Model" : modelNameText.stringValue
modelName.onNext(model)
}
}
func parse() {
Observable
.combineLatest(source, modelName) { (json: $0.0, modelName: $0.1) }
.map { (self.json(with: $0.json), $0.modelName) }
.map(convert)
.subscribe(onNext: {
self.resultText.string = $0
})
.addDisposableTo(bag)
}
我用 combineLatest 把两个 Subject 组合在一起,无论哪个 Subject 发出新事件,都会接收到该新事件和另一个 Subject 的发射过的最新事件。具体效果就是,只有在左侧把 Model Name 和 JSON 都填上,右边才会显示结果,之后无论是改变 Model Name 还是 JSON 内容,右侧结果都会跟着变化。
最后
觉得有点意思的话可以随手 Star 一个哈 ^ ^