This code uses Swift 4

Code: https://github.com/jamesdouble/RandMyMod


preface

Since the release of Swift 4, many readers have read countless articles in China and abroad about what is new for Swift 4 in Codable. It’s just a shortcut to a custom Json -> model that is currently common in the business of calling back to the server, and adds a lot of flexibility. This article is not focused on Codable agreements or conversions, but Codable can help me with the following questions, so I won’t cover more in detail.

Custom Encoding and Decoding

Only care about Codable for jumping into Codable for random custom models

Problem

Recently, I have done several tests similar to unit development at work, which need a lot of tests to see whether UI can be displayed properly for various types of data, and need a framework that can randomize its model data.

Of course, the first time is to look for a suitable framework that can be used 😅😅. After searching various keyword combinations, there is still no one that can meet this requirement (maybe I did not find it, please refer to it here).

Finally I decided to write a highbrow version of the model random framework.

purpose

struct MyStruct {
	var opt: Int
	var opt2: Int} foo = function()MyStruct)

foo.opt  / / 4242
foo.opt2 / / 1234

Copy the code

A preliminary Approach

To be able to do their own custom model to the program to random, you must start from the dynamic call, otherwise the framework will not be able to know your custom model variable name, variable type… And so on, and can’t scramble for your variables.

  • Here are a few common dynamic calls to try and compare their strengths and weaknesses:

    Using Mirror + Protocol alone lacks many known elements

    For static calls to Swift, Mirror is a bit of a special case, and while it doesn’t have a Runtime (Apple calls it reflection), it’s a very useful framework.

    • Advantages:

      Compatible with any custom Struct, Class, and is pure Swift.

    • Disadvantages:

      Because there is no restriction on a particular known type, only its variable type can be identified and its value read, with no channel for the reverse assignment.

      Poor performance.

      There’s no way to do tree processing.

    Manual assignment using Protocol

    It is not feasible to manually determine each returned key and then assign it.

    The same disadvantage, use Mirror must put a full instance, can not only pass Type, write out in fact is not pure.

    protocol RandProtocol {
    	mutating func randResult(key: String, value: Any)
    	static func initRand(a) -> 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(a) -> RandProtocol {
            	return JamesStruct()}}class RandMyMod<T: RandProtocol> {
    
    	func randByMirror(a) -> 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
    
    Copy the code

    Most convenient but limited NSObject

    • Advantages:

      1. Inherited classes can share the init() method of NSObject, so that instead of passing in an instance, you can simply initialize an instance T() from Type T and assign to it.

      2. You have a method.value(forKey:), and you can get the value of the variable if you can get the name of the variable, and you can use that value to determine what type of value you’re going to set, and of course in NSObject it’s not hard to get the name of the variable.

    • Disadvantages:

      1. NSObject must be inherited, but my preference for using structs to implement most of the Data model means that there is no way to use any inheritance.

      2. Any custom model variables under this Class must also be NSObject in order to do tree randomness, otherwise it will stop.

      class A: NSObject { 
      	var foo: B	// This variable is not recognized and cannot be changed
      }
      
      class B {
      	var num: Int
      }
      Copy the code

    With the Runtime

    class James: NSObject {
        @objc var opt: Int = 0
    	@objc var opt2: Int = 0
    }
    
    class RandMyMod<T: NSObject> {
    	func rand(a) -> T {
       	var count: UInt32 = 0
        	var newObject: T = T(a)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
    Copy the code

    When it comes to the most complete and op dynamic calls, the Objective-C Runtime is definitely the least set up and the most intuitive.

    1. At present, the overall direction of the framework still hopes to use pure Swift instead of OC method.
    2. A framework that uses RunTime is still too intrusive for the main program, and hopefully it will be dominated by ancillary utility classes.

    With Mirror

    class RandMyMod<T: NSObject> {
    
    	func randByMirror(a) -> T {
        	var newObject: T = T(a)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
    Copy the code

    Mirror is much easier than it looks to get variable names and variable types, but its limitations are almost the same as those of Runtime, and even worse than Runtime. If there are too many variables, it will affect the running of the thread.

Average demand is Codable

First of all, Codable has nothing to do with dynamic calls. I didn’t realize that at first, but after averaging out these two features for Codable, I found out that there are several optimizations for Codable:

  1. Struct and Class are available for use in Codable protocol
  2. Json automatically generates model instances. Json automatically assigns to a model. There is nothing wrong with that
  3. Not using a Mirror or Runtime is inefficient, almost a simple change of value

The only two things that couldn’t have been better (or that I hadn’t thought of) are:

  1. You cannot initialize an instance using Codable Type alone
  2. To make a tree of random variables, they must also be Codable.
class Man: Codable {
    var name: String = ""
    var address: String = ""
    var website: [String] = []}let man = Man(a)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"]
}
Copy the code

Implement

The whole process is simple:

  1. Instance encode becomes Data

    func randMe(baseOn instance: T, completion: (T?)- > ()) {let jsonData = try JSONEncoder().encode(instance)
    Copy the code
  2. The Data into a Dictionary

    let jsonDic = try JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers)
    Copy the code
  3. Element of random Dictionary

    This framework handles recursion randomization in a tree and includes some additional functionality for Codable versions of the most complex assignment problems that are already standard.

    • Tree recursion random

      The main processing is only in this class RandFactory:

      Init (inject a Value: Any, key: String), and call function-randdata () to get a random Value of the same type.

      Injectable types include String, Int, Float… Random types, including, most importantly, the Dictionary.

      If the type is Dictionary**(which means it is also a custom Codable model), loop through the elements in the block, RandFactory them once again, and update the Dictionary with the new value for recursion.

      for (_, variable) in dictionary.enumerated() {
      	let factory = RandFactory(variable.value, variable.key, specificBlock: specificBlock, delegate: delegate).randData()
      	dictionary.updateValue(factory, forKey: variable.key)
      }
      Copy the code
    • Additional features

      You can do this by ignoring specific variables and specifying type random seeds…. Etc., not to be repeated here

  4. The Dictionary into the Data

    let jsonData = try JSONSerialization.data(withJSONObject: newDicionary, options: .prettyPrinted)
    Copy the code
  5. Data Decode into an instance

    let decoder = JSONDecoder(a)let newinstance = try decoder.decode(T.self, from: jsonData)
    Copy the code

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(a)RandMyMod<Man>().randMe(baseOn: man) { (newMan) in
    guard letchild = newMan? .childelse { print("no"); return }
    print(child.name)	//Baby
    print(child.age)	/ / 3
    print(child.toy.weight)	/ / 392.807067871094
}

Copy the code