美文网首页
(IOS) 别只将Codable用来解Json, 玩转你的模型吧

(IOS) 别只将Codable用来解Json, 玩转你的模型吧

作者: JamesDouble | 来源:发表于2018-04-24 23:05 被阅读153次
    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
    }
    
    

    相关文章

      网友评论

          本文标题:(IOS) 别只将Codable用来解Json, 玩转你的模型吧

          本文链接:https://www.haomeiwen.com/subject/pxidlftx.html