(iOS) 別隻將Codable用來解Json, 玩轉你的模型吧 從0到Double系列

jamesdouble發表於2018-04-27

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
}

複製程式碼

相關文章