本文代码使用Swift 4
代码:https://github.com/jamesdouble/RandMyMod
前言
自从 Swift 4 出来之后(现已4.1),相信不少读者已经看过无数国内外篇的文章在介绍 Swift 4 当中的一个新功能 Codable
,之所以会火,不外乎就是为一个目前普遍业务上对伺服器回调Json -> 自定义模型这个流程开了一条捷径,也附加了不少弹性。本篇文章不是着重于Codable的协议或是转换,而是Codable能帮助我解决以下的问题
,所以其他就不赘述了。
只关心Codable可跳到用Codable随机自定义模型
Problem
最近在工作上做了几个类似单元开发的测试,需要大量的测试面对各型各样的数据UI能否正常展示,需要一个能将自己模型数据随机化的一个框架
。
第一时间当然是找找有没有符合的框架能用😅😅,各种关键字组合搜索后,还是没看到能达成这块需求的(可能是我没搜索到,在这也求介绍),
最后还是决定自己来写一个阳春版的模型随机框架。
目的
struct MyStruct {
var opt: Int
var opt2: Int
}
foo = 某function(MyStruct)
foo.opt // 4242
foo.opt2 // 1234
初步Approach
要能做到把自己自定义的模型交给程序去随机,就一定得从动态调用
起手,否则框架就无法得知你自定义的模型 变量名,变量的型别...等,进而无法针对你的变量打乱。
-
以下我就尝试了几个常用的动态调用,比较他们各自的优缺:
单纯使用 Mirror + Protocol 缺少诸多已知要素
在静态调用环境下的Swift, Mirror算是比较特殊了,虽然没有Runtime牛(Apple管它叫做
反射
),但也是个很好用的框架。-
优点:
可兼容
任何自定义的Struct, Class
,且是纯Swift。 -
缺点:
因为不去限制特定的已知类型,故只能识别它的变量类型跟读取变量数值,并没有附带反过来赋值的通道。
性能较差。
没有办法做树状处理。
使用Protocol手动赋值
需要手动去
判断每个回传回来的key再自行去赋值
,完全是不可行的方法。同上缺点,使用Mirror一定得放尽一个实例,不能只传Type,全写出来其实也不单纯了。
protocol RandProtocol { mutating func randResult(key: String, value: Any) static func initRand() -> RandProtocol } struct JamesStruct: RandProtocol { var opt: Int = 0 mutating func randResult(key: String, value: Any) { if key == "opt" { if let intvalue = value as? Int { self.opt = intvalue } } } static func initRand() -> RandProtocol { return JamesStruct() } } class RandMyMod<T: RandProtocol> { func randByMirror() -> T { guard var newObject: T = T.initRand() as? T else { fatalError() } let mirror = Mirror(reflecting: newObject) for child in mirror.children { if child.value is Int { newObject.randResult(key: child.label!, value: (Int(arc4random_uniform(100) + 1))) } else if child.value is Float { /// } } return newObject } } let james = RandMyMod<JamesStruct>().randByMirror() james.opt
最方便但限制多 NSObject
-
优点:
继承的Class可以共用NSObject的init()方法,这样就
不需要在使用时要传进一个实例
,能直接从Type T 初始化一个实例 T(),即可对他进行赋值。拥有方法
.value(forKey:)
,只要能取得变量名称就能取得变量数值,进而用此数值判断之后要set的Value是什么样的类型,当然在NSObject里要取得变量名称也不难。
-
缺点:
必须要继承 NSObject,但个人偏好使用 struct 来实作大部分的Data模型,也代表没办法使用任何继承。
在此Class下的任何自定义模型的变量也必须要是NSObject,才能做
树状
的随机,否则将停止。
class A: NSObject { var foo: B //此变量无法被识别, 也无法被更改 } class B { var num: Int }
搭配Runtime
class James: NSObject { @objc var opt: Int = 0 @objc var opt2: Int = 0 } class RandMyMod<T: NSObject> { func rand() -> T { var count: UInt32 = 0 var newObject: T = T() guard let properties = class_copyPropertyList(T.self, &count) else { fatalError() } for i in 0..<count { let pro = properties[Int(i)] let name = property_getName(pro) let str = String(cString: name) newObject.setValue(Int(arc4random_uniform(100) + 1), forKey: str) } return newObject } } let james = RandMyMod<James>().rand() james.opt // == 20 james.opt2 // == 45
说到最完善最op的动态调用,肯定是
Objective-C向的Runtime
了,设置最少也最直观,我之所以最后不采用的原因:- 目前框架整体方向还是希望能以纯Swift为主,不想使用类OC方法。
- 有使用RunTime的框架对于主程序的侵入性还是较大的,希望此框架是以辅助性的工具类为主。
搭配Mirror
class RandMyMod<T: NSObject> { func randByMirror() -> T { var newObject: T = T() let mirror = Mirror(reflecting: T()) for child in mirror.children { if child.value is Int { newObject.setValue(Int(arc4random_uniform(100) + 1), forKey: child.label!) } else if child.value is Float { /// } } return newObject } } let james2 = RandMyMod<James>().randByMirror() james2.opt // == 55 james2.opt2 // == 74
Mirror 在取得变量名称与变量类型的判断上明显比看起来简易许多,但他的局限性其实跟Runtime差不多,效能甚至比Runtime跟差,若是变量数量较多会导致影响到线程的运行。
-
平均需求 Codable
先说Codable跟动态调用其实一点毛关系也没有,第一时间也是没想到的,但平均了以上两个的优缺,我却发现Codable能涵盖几个问题的优化:
- Struct, Class 都能使用,因为Codable是协议
- 赋值问题,说是json自动生成模型实例,那理解成用json自动赋值给一个模型,没啥毛病
- 没有使用Mirror 或是 Runtime 效能消耗很低,几乎是单纯的改值而已
唯一比较没法在更优化(或是我没想到)的两点:
- 无法单纯使用 Codable type 去初始化一个实例
- 若要做树状的随机,变量也得是Codable。
class Man: Codable {
var name: String = ""
var address: String = ""
var website: [String] = []
}
let man = Man()
RandMyMod<Man>().randMe(baseOn: man) { (newMan) in
guard let new = newMan else { return }
print(new.address) //mnxvpkalug
print(new.name) //iivjohpggb
print(new.website) //["pbmsualvei", "vlqhlwpajf", "npgtxdmfyt"]
}
Implement
整个流程很单纯就是:
-
实例 encode 成 Data
func randMe(baseOn instance: T, completion: (T?)-> ()) { let jsonData = try JSONEncoder().encode(instance)
-
Data 转成 Dictionary
let jsonDic = try JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers)
-
随机Dictionary的元素
基本上原本最复杂的取值赋值问题,Codable都已经大家常用的标准方法了,所以本框架就只需要在这一步,处理树状的递回随机,并着墨一些附加功能。
-
树状的递回随机
主要处理只在这个类RandFactory:
它的init(注入一个 Value: Any, key变量名: String),外部调他的function - randData() 即可获得跟注入同类型的,且已随机的值。
可注入的类型包括
String, Int, Float….
等可随机的类型,还包括最重要的Dictionary
。若类型是 Dictionary(类型是字典,代表他也是一个自定义的Codable模型),遍历里面的元素,将元素在做一次RandFactory,并用新值更新Dictionary,达到递回。
for (_, variable) in dictionary.enumerated() { let factory = RandFactory(variable.value, variable.key, specificBlock: specificBlock, delegate: delegate).randData() dictionary.updateValue(factory, forKey: variable.key) }
-
附加功能
可以做到忽略特定变量,指定类型随机种子....等,这里不赘述
-
-
Dictionary 传成 Data
let jsonData = try JSONSerialization.data(withJSONObject: newDicionary, options: .prettyPrinted)
-
Data Decode 成 实例
let decoder = JSONDecoder() let newinstance = try decoder.decode(T.self, from: jsonData)
Final Example
struct Man: Codable {
var name: String = ""
var age: Int = 40
var website: [String] = []
var child: Child = Child()
}
struct Child: Codable {
var name: String = "Baby" //Baby has no name yet.
var age: Int = 2
var toy: Toys = Toys()
}
class Toys: Codable {
var weight: Double = 0.0
}
extension Man: RandMyModDelegate {
func shouldIgnore(for key: String, in Container: String) -> Bool {
switch (key, Container) {
case ("name","child"):
return true
default:
return false
}
}
func specificRandType(for key: String, in Container: String, with seed: RandType) -> (() -> Any)? {
switch (key, Container) {
case ("age","child"):
return { return seed.number.randomInt(min: 1, max: 6)}
case ("weight",_):
return { return seed.number.randomFloat() }
default:
return nil
}
}
}
let man = Man()
RandMyMod<Man>().randMe(baseOn: man) { (newMan) in
guard let child = newMan?.child else { print("no"); return }
print(child.name) //Baby
print(child.age) //3
print(child.toy.weight) //392.807067871094
}