In the development of iOS projects, we often use the operation of converting json from the server to the model, We can use the Swift provides setValuesForKeys or Objective – C provide setValuesForKeysWithDictionary method to complete this operation.

Using the above two methods can only convert a dictionary to a Model. If the json outermost layer is an array, then we have to use this method in a loop, which is very inconvenient, as well as the condition that all the property names in the Model must correspond exactly to the keys in the dictionary. Another problem is that if a key in our dictionary has the same name as a system key, we can’t use that key as a property name in Model.

To solve the above problem, we will use some third-party libraries such as MJExtension to complete the dictionary to model operation. Because it is an OC library, the bridge file needs to be introduced in the Swift project. Using its API in Swift is actually very un-swift. So now let’s use Swift 3.0 to write a Swift style JSON-to-model library.

For example, we have two models like this:

class User: NSObject {
    var name: String?
    var age = 0
    var desc: String?
}
class Repos: NSObject {
    var title: String?
    var owner: User?
    var viewers: [User]?
}Copy the code

Eventually we want to implement a call like this:

let repos = json ~> Repos.self    // Convert a dictionary into an instance of Repos

let viewers  = viewers => User.self  // Convert an array to an array of usersCopy the code

~> and => are custom operators for ease of call. They are defined like this:

public func ~><T: NSObject>(lhs: Any, rhs: T.Type) -> T?
public func =><T: NSObject>(lhs: Any, rhs: T.Type)- > [T]?Copy the code

Here is my implementation of ModelSwift. You can look at my implementation and try to write your own implementation. Okay, now let’s get started.

Problems to be solved

Since converting an array to a model array is exactly the same as converting a dictionary to a model, it’s just a loop. So the first problem we have to solve is how to convert dictionaries into models in Swift. In this case, we’ll just use KVC. We use NSObject setValue(_ value: Any? , forKey key: String) to set the value of the object.

From the point of view above, we do not need to instantiate an object before using it. So the second problem we have to solve is how to instantiate an object by type.

Another problem to solve is that the key in the dictionary has the same name as the keyword, or we want to use our own name. So we have to implement our own mapping strategy.

Another problem is that if the dictionary data returned by our server contains another dictionary array, our model is an array of one object containing another object. So how do we know the types of the objects in this array?

Implementation approach

The solution to the first problem I mentioned above is to have our model inherit NSObject and then use setValue(_ value: Any? , forKey key: String) to set the value of the object. The value is actually retrieved from the dictionary based on the property name in the model. If we can get all of model’s property names, we are done setting values for Model. So how do I get the model property name? This requires the use of the reflection mechanism in Swift.

Mirror

Swift’s reflection mechanism is based on a struct called Mirror. For the detailed structure of the Mirror you can press CMD to view. Public TypeAlias Child = (Label: String? , value: Any) this typealias, which is actually a meta-ancestor, label is our property name, it’s Optional. Value represents the value of an attribute. Why is the label Optional? This actually makes a lot of sense if you think about it. Not all data structures that support reflection contain named child nodes. A Mirror uses the name of an attribute as a label, but a Collection has only subscripts and no names. Tuples may also not have names for their entries.

Mirror has a children store property, which is defined like this:

 public let children: Mirror.ChildrenCopy the code

The mirror. Children here is also a typeAlias, which is defined like this:

public typealias Children = AnyCollection<Mirror.Child>Copy the code

You can see that it’s a collection of children. So we can get all the model property names from the Mirror’s children property.

Let’s write a class to test this:

class Person: NSObject {
    var name = ""
    var age = 0
    var friends: [Person]?
}

let mirror = Mirror(reflecting: Person())
for case let(label? , value)in mirror.children {
    print ("\(label) = \(value)")}Copy the code

The result is as follows:

name = 
age = 0
friends = nilCopy the code

Mirror also has a subjectType store property of Type any. Type that represents the Type of the mapped object, for example, mirror.subjectType is User. Using subjectType, you can obtain the type of the object and the types of all its properties. To achieve this effect, we can write the following code:

func subjectType(of subject: Any) -> Any.Type {
    let mirror = Mirror(reflecting: subject)
    return mirror.subjectType
}

func children(of subject: Any) {
    let mirror = Mirror(reflecting: subject)
    for case let(label? , value)in mirror.children {
        print ("\(label) = \(subjectType(of: value))")
    }
}

children(of: Person())Copy the code

The print looks like this:

name = String
age = Int
friends = Optional<Array<Person>>Copy the code

I originally wanted to use this method to get the types of other objects contained in model and the types of objects in array, such as the father and friends attributes in Person:

class Person: NSObject {
    var name = ""
    var age = 100
    var father: Person?
    var friends: [Person]?
}Copy the code

But the results are Optional and Optional

>. So we still have to explicitly indicate the types of other objects contained in a Model, as well as the types of objects in the array. I’ll present my own implementation later. You can give your own implementation.

Instantiate an object by type

To use a Mirror to get all the attribute names of a reflected object, you must first create a Mirror with init(reflecting subject: Any). To create a Mirror, you must pass in a Subject (in this case, an object of type NSObject). So our first task is to instantiate an object by type.

If I want to convert a Person object, I’ll just pass in an instance of Person. If you look at josn’s definition of how to model. func ~>

(lhs: Any, rhs: T.Type) -> T?

Using the Person example above, let’s look at a call like this:

Person.self().age
// The result is 100Copy the code

So we can get an instance of a class through the self() method of a class. We can also instantiate objects using AnyClass. AnyClass is the type of the class, which is defined like this:

public typealias AnyClass = AnyObject.TypeCopy the code

We can get the type of the class from the class self attribute:

Person.self     
// Result: person.typeCopy the code

Once you have the type of the class, you can create an instance by calling its init() method:

Person.self.init(a).age
// The result is 100Copy the code

Init methods in classes that create objects using types must be preceded by required, because the creation method is created using meta Type. Since our JSON-to-model model inherits from NSObject, we don’t have to explicitly implement it in every class.

Write a simple josn transformation

With this foundation, we can implement our JoSN transformation model. First we write the definition of ~> and create an object from the class

infix operator~ >func ~><T: NSObject>(lhs: Any, rhs: T.Type) -> T? {
    guard let json = lhs as? [String: Any], !json.isEmpty else {
        return nil
    }

    let obj = T.self(a)let mirror = Mirror(reflecting: obj)

    for case let(label? , value)in mirror.children {
        print ("\(label) = \(value)")}return obj
}

class Person: NSObject {
    var name = ""
    var age = 0

    override var description: String {
        return "name = \(name), age = \(age)"}}let json: [String: Any] = ["name": "jewelz"."age": 100]
let p = json ~> Person.self
// Print the result:
// name = 
// age = 0Copy the code

With the few lines above we have successfully created an instance of Person. The next step is to set the values for the instance. We add the following code to the for loop above:

If let value = json[label] {
     obj.setValue(value, forKey: label)
}Copy the code

The entire code looks like this:

infix operator~ >func ~><T: NSObject>(lhs: Any, rhs: T.Type) -> T? {
    guard let json = lhs as? [String: Any], !json.isEmpty else {
        return nil
    }

    let obj = T.self(a)let mirror = Mirror(reflecting: obj)

    for case let(label? ._) in mirror.children {
        // Get the value from the dictionary
        if let value = json[label] {
            obj.setValue(value, forKey: label)
        }
    }
    return obj
}

let p = json ~> Person.self
print(p!)
// Result: name = jewelz, age = 100Copy the code

With the above implementation of ~>, the => implementation is simple:

infix operator= >func =><T: NSObject>(lhs: Any, rhs: T.Type)- > [T]? {
    guard let array = lhs as? [Any], !array.isEmpty else {
        return nil
    }

    return array.flatMap{ $0 ~> rhs }
}Copy the code

The above is just a simple josn transformation model, but there are many problems to be solved in the actual project. Now look again at the User and Respo classes I presented at the beginning of this article:

class User: NSObject {
    var name: String?
    var age = 0
    var desc: String?
}
class Repos: NSObject {
    var title: String?
    var owner: User?
    var viewers: [User]?
}Copy the code

Simply using the above implementation will not achieve the desired results. For the User class, the DESC attribute corresponds to the JSON description key, so we need to map the Model attribute to the JSON key. The idea here is to store the model property name as the key and the JSON key to be replaced as the value in the dictionary. We can extend NSObject to add a calculated property and provide an empty implementation. However, this is too intrusive, not all classes need to do this mapping. So the last way is POP. For example, we could make an agreement like this:

public protocol Reflectable: class {
    var reflectedObject: [String: Any.Type] { get}}Copy the code

Implement the protocol in the classes that need to be mapped.

For more complex Repos classes, there is more to do. How do you know the type of owner? How does the owner object get assigned? What are the types in the Viewers array, and how can assignments be completed? Although you can get all the types from Mirro mentioned above, you get Optional

and Optional

>. My solution is the same as doing the property name substitution above. I’m not going to go into the details here, but you can do your own thing. Write your own implementation.

Write in the last

With the above steps, we can quickly implement a simple JSON to model requirement. To sum up, the following points:

  • All models to be transformed inherit NSObject
  • Instantiate objects using the type of the class
  • Get all the property names of the object by reflection
  • throughsetValue(_ value: Any? , forKey key: String)Method to set a value for a property

I will not elaborate here on the last few questions raised. You can click here to see my implementation. You can use CocoaPods or Carthage to integrate ModelSwift into your project. If there is any problem in use, you can issue me or give a STAR for continuous attention.