(IOS) 别只将Codable用来解Json, 玩转你的模型吧 - 从0到Double系列

logo.png

本文代码使用Swift 4

代码:https://github.com/jamesdouble/RandMyMod


前言

自从 Swift 4 出来之后(现已4.1),相信不少读者已经看过无数国内外篇的文章在介绍 Swift 4 当中的一个新功能 Codable,之所以会火,不外乎就是为一个目前普遍业务上对伺服器回调Json -> 自定义模型这个流程开了一条捷径,也附加了不少弹性。本篇文章不是着重于Codable的协议或是转换,而是Codable能帮助我解决以下的问题,所以其他就不赘述了。

Custom Encoding and Decoding

只关心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

    • 优点:

      1. 继承的Class可以共用NSObject的init()方法,这样就不需要在使用时要传进一个实例,能直接从Type T 初始化一个实例 T(),即可对他进行赋值。

      2. 拥有方法.value(forKey:),只要能取得变量名称就能取得变量数值,进而用此数值判断之后要set的Value是什么样的类型,当然在NSObject里要取得变量名称也不难。

    • 缺点:

      1. 必须要继承 NSObject,但个人偏好使用 struct 来实作大部分的Data模型,也代表没办法使用任何继承。

      2. 在此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了,设置最少也最直观,我之所以最后不采用的原因:

    1. 目前框架整体方向还是希望能以纯Swift为主,不想使用类OC方法。
    2. 有使用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能涵盖几个问题的优化:

  1. Struct, Class 都能使用,因为Codable是协议
  2. 赋值问题,说是json自动生成模型实例,那理解成用json自动赋值给一个模型,没啥毛病
  3. 没有使用Mirror 或是 Runtime 效能消耗很低,几乎是单纯的改值而已

唯一比较没法在更优化(或是我没想到)的两点:

  1. 无法单纯使用 Codable type 去初始化一个实例
  2. 若要做树状的随机,变量也得是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

整个流程很单纯就是:

  1. 实例 encode 成 Data

    func randMe(baseOn instance: T, completion: (T?)-> ()) {
      let jsonData = try JSONEncoder().encode(instance)
    
  2. Data 转成 Dictionary

    let jsonDic = try JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers)
    
  3. 随机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)
      }
      
    • 附加功能

      可以做到忽略特定变量,指定类型随机种子....等,这里不赘述

  4. Dictionary 传成 Data

    let jsonData = try JSONSerialization.data(withJSONObject: newDicionary, options: .prettyPrinted)
    
  5. 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
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • 1、随机数 不需要随机数种子 arc4random()%N + begin:产生begin~begin+N的随机数...
    我是小胡胡分胡阅读 4,134评论 0 2
  • SwiftDay011.MySwiftimport UIKitprintln("Hello Swift!")var...
    smile丽语阅读 3,826评论 0 6
  • importUIKit classViewController:UITabBarController{ enumD...
    明哥_Young阅读 3,771评论 1 10
  • 132.转换错误成可选值 通过转换错误成一个可选值,你可以使用 try? 来处理错误。当执行try?表达式时,如果...
    无沣阅读 1,232评论 0 3
  • 今天上午公司组织了一场国学文化分享活动,请来了台湾的国学老师,跟大家讲讲《论语》,谈谈体会。 听完还是挺有感触的,...
    语竹123阅读 334评论 1 1