For code readability and development efficiency, we tend to abstract data into a data model and manipulate the data model rather than the data itself during development.

During development, we need to convert key-value structure data, namely dictionaries, into a data model. That’s dictionary to model.

The dictionary transformation model is mainly applied in two scenarios. Network requests (JSON parsed to model, model to dictionary as request parameters), persistent access to model data.

Now let’s discuss several mainstream dictionary-model conversion methods in OC and SWIFT respectively.

1. The way of dictionary transformation to model in SWIFT

1.1 Codable

Codable is the first type introduced for Swift4, which includes two protocols Decodable Encodable

public typealias Codable = Decodable & Encodable

public protocol Encodable {
    func encode(to encoder: Encoder) throws
}

public protocol Decodable {
    init(from decoder: Decoder) throws
}
Copy the code

Codable makes it easy to decode and encode data models into data.

Make the model conformCodableEverything will be easy

Encodable and Decodable have default implementations. When we make our models Codable, they can be coded and decoded.

class User : Codable { var name : String? . }Copy the code

Json transfer model

let model = try? JSONDecoder().decode(User.self, from: jsonData)
Copy the code

Model turn json

let tdata = try? JSONEncoder().encode(model)
Copy the code

More often than not, we’re dealing with dictionary-to-model, handing the JSON parsing step over to the network request library. Codable also parsed json into dictionaries before converting them into models.

So the way we usually use it is like this:

Func decode<T>(json:Any)->T?where T:Decodable{
    do {
        let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
        let model = try JSONDecoder().decode(T.self, from: data)
        return model
    } catch let err {
        print(err)
        returnFunc encode<T>(model:T) ->Any?where T:Encodable {
    do {
        let tdata = try JSONEncoder().encode(model)
        let tdict  = try JSONSerialization.jsonObject(with: tdata, options: JSONSerialization.ReadingOptions.allowFragments)
        return tdict
    } catch let error {
        print(error)
        return nil
    }
}
Copy the code
How do we get our model to complyCodable

When our models claim to conform to Codable protocols, they are most likely to conform to Codable dable models. How do we make our models conform to Codable protocols

Have to comply with theCodableTypes in the system library of:
  • All the base types, Int,Double, String…
  • Most types in swift Foundation: URL, Data, Date, IndexPath, etc., do not include OC types
  • Collection types: Array,Dictionary<Key, Value>, Set, etc. It is important to note that all contained subtypes are subject to complianceCodable
In compliance withCodable, we need to pay attention to:
  • All attribute types must complyCodable. In addition to giving us our custom subclassesCodableAdditional types must be one of the Codable types listed above
  • Enum types must implement RawRepresentable, which means that raw value types need to be defined. And the original value type must also be followedCodable
If required include not followedCodableHow attributes are handled

In projects, we sometimes need to declare non-codable attributes in the model, such as CLLocation and AVAsset. Next, let’s look at the possible solutions to these needs

  • Computed properties do not visualize pairsCodableImplementation of the protocol. We don’t care if it’s Codable for any calculated property
  • Not through extensionCodableType add complianceCodableThe statement of
    • This extension is supported for Encodable protocols, except that the class type must be declared final (Decodable is not supported).

    • But it comes with the harsh condition that extension must be in the same file as the type declaration.

Implementation of ‘Decodable’ cannot be automatically synthesized in an extension in a different file to the type

- This approach is obviously not feasible unless we can modify the corresponding type of source codeCopy the code
  • Implements CodingKeys and does not include non-Codable attributes.

    • CodingKeys can be compiled if they do not contain cases that are not Codable:

      struct Destination : Codable {
          var location : CLLocationCoordinate2D?
          var city : String?
          enum CodingKeys : String,CodingKey {
              case city
          }
      }
      Copy the code
  • Make your own Encodable and Decodable transformations

    • Proven, as long as manual implementationEncodableandDecodableMethods in the protocol will not fail any claims for non-Codable attributes. What we need to do is convert specific data structures to and from non-codable type attributes
When does conversion fail
  • Inconsistent type: An exception is thrown when the type of the property declared is inconsistent with that of the corresponding field in the dictionary
  • An exception is thrown when a non-empty attribute has no corresponding key in the dictionary, or value is empty
Special handling of key
  • If you do not want all keys to be codec, or if the dictionary has different keys and attribute names.

You can declare a CodingKeys enumeration.

enum Codingkeys : String,CodingKey {
    case id,name,age
    case userId = "user_id"
}
Copy the code

Once this is done, only all declared cases are processed during the transformation process, and the mapping between keys and attributes is determined based on the original values.

By default, an enumeration type CodingKeys that extends to CodingKey protocol is synthesized for cidcidr, which contains all declared property values (excluding static variables), and stringValue is the same as the property name.

The default implementation of CodingKeys is private and can only be used within a class

‘CodingKeys’ is inaccessible due to ‘private’ protection level

The default implementations for Decodable and Encodable are based on CodingKeys. If we declare it ourselves, the system will use our declared CodingKeys instead of generating them automatically.

CodingKeys does not conform to the protocol if the CodingKeys contain cases that do not exist in the CodingKeys attribute.

  • It’s just the naming style

Direct setup through JSONDecoder Settings can be completed

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
Copy the code

It’s going to convert all the keys with the underlined separator into a camel’s name

Special handling of value
Some value types that can be converted automatically
  • Enum type: The rawValue can be automatically converted to an enum value
  • URL type: Can automatically convert a string to a URL
  • Date Date type

By default can be Double type, that is, on January 1, 2001 to the present the timestamp (timeIntervalSinceReferenceDate), into the Date type. We can use JSONDecoder’s dateDecodingStrategy property to determine the parsing strategy of Date type

public enum DateDecodingStrategy {
    /// default strategy.
    case deferredToDate
    
    case secondsSince1970
    case millisecondsSince1970
    case formatted(DateFormatter)
    case custom((Decoder) throws -> Date)
    
    ///  ISO-8601-formatted string 
    case iso8601
}
Copy the code
Implement protocol methods manually

This is enough for most business scenarios, but we need to convert attribute values in coding and decoding or perform multi-level mapping. We need to implement Encodable ‘ ‘Decodable’ protocol methods to convert attribute values

struct Destination:Codable {
    var  location : CLLocationCoordinate2D

    private enum CodingKeys:String,CodingKey{
        case latitude
        case longitude
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy:CodingKeys.self)
        try container.encode(location.latitude,forKey:.latitude)
        try container.encode(location.longitude,forKey:.longitude)
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let latitude = try container.decode(CLLocationDegrees.self,forKey:.latitude)
        let longitude = try container.decode(CLLocationDegrees.self,forKey:.longitude)
        self.location = CLLocationCoordinate2D(latitude:latitude,longitude:longitude)
    }
    
}
Copy the code
1.2 ObjectMapper

ObjectMapper is a JSON-to-model library written in Swift. It relies primarily on Mappable and all types that implement this protocol can easily convert JSON to model

use

First implement Mappable in the class

class UserMap : Mappable { var name : String? var age : Int? var gender : Gender? var body : Body? required init? (map: Map) {} // Mappable func mapping(map: Map) { name <- map["name"]
        age         <- map["age"]
        gender      <- map["gender"]
        body        <- map["body"]}}Copy the code

Dictionary model

let u = UserMap(JSON:dict)
Copy the code

Model to dictionary

letjson = u? .toJSON()Copy the code
The advantages and disadvantages
  • If it’s a Swift project, don’t worry about bridging, the code looks swifty.
  • The addition, deletion and modification of dictionary attributes have little effect on the transformation process. When a dictionary type is inconsistent with an attribute type, the attribute is given a nil value, leaving the other attributes unaffected
  • The disadvantages are also obvious to realizefunc mapping(map: Map)Method to determine the mapping relationship between attributes and keys, this step will be particularly tedious, especially in the case of multiple attribute values
Automated code generation

Of course, someone has automated this tedious process

  • Objectmapper-plugin a Plugin that automatically adds the Mappable implementation to the current type code

  • Json4swift a site that automatically generates swift model code. It is a very useful tool for json to data model code. Support for Codable, ObjectMapper, and Classic Swift Key/Value dictionaries

1.3 HandyJSON

Similar to Codable, make your models follow HandyJSON to convert dictionaries and json to models.

In principle, it mainly uses Reflection, relying on memory rules inferred from the Swift Runtime source code

This is ali’s project, and interested students can go to his Github

2. The way of dictionary model transformation in OC
2.1 KVC

KVC, which stands for KeyValueCoding, is an informal protocol defined in nsKeyValuecoding. h. KVC provides a mechanism for indirectly accessing its attribute methods or member variables through strings.

Its core approach is:

- (void)setValue:(nullable id)value forKey:(NSString *)key;

- (nullable id)valueForKey:(NSString *)key;
Copy the code

In the dictionary model method, we are familiar with:

Person *person = [[Person alloc] init];
[person setValuesForKeysWithDictionary:dic];
Copy the code

It is equivalent to

Person *p0 = [[Person alloc] init];
for (NSString *key in dic) {
    [p0 setValue:dic[key] forKey:key];
}
Copy the code

The only difference is that if null, exist in the dictionary or json use setValue: forKey: when the NSNull attribute values, and setValuesForKeysWithDictionary: get the corresponding nil value of the attribute type

Model to dictionary, can be used:

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
Copy the code

In use, note the following:

  • Keys in the dictionary must correspond to object property names one by one
  • To avoid crashes caused by unnecessary keys in the dictionary, it is common to add:
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{}
Copy the code
  • For value type attributes, we generally declare them as the corresponding value types, such as int, double, BOOL, CGRect, etc., rather than NSNumber or NSValue. You can convert correctly regardless of which way you declare it.
Nested types

KVC does not support direct conversions for nested types, but we can do so by overriding setter methods for the corresponding properties

- (void)setBody:(Body *)body
{
    if(! [body isKindOfClass:[Body class]] && [body isKindOfClass:[NSDictionary class]]) { _body = [[Body alloc] init]; [_bodysetValuesForKeysWithDictionary:(NSDictionary *)body];
    }else{ _body = body; }}Copy the code

You can use a similar method if the value needs to be converted

The key value transformation

This can be done by overwriting setValue:forUndefinedKey when there is a difference between the key value in the dictionary and the attribute name

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    if ([key isEqualToString:@"id"]) { self.ID = value; }}Copy the code
Used in SWIFT

To be used in Swift, first the model class must inherit from NSObject, and all required properties must have the @objc modifier

2.2 MJExtension
2.2.1 Usage

MJExtension is the most commonly used third-party library for dictionary transformation models in OC. It’s very simple to use.

// User * User = [User mj_objectWithKeyValues:dict]; NSDictionary *userDict = user.mj_keyvalues;Copy the code

It also supports many unique features compared to using KVC directly

A nested model

It supports the following nested model.

@interface Body : NSObject
@property (nonatomic,assign)double weight;
@property (nonatomic,assign)double height;
@end

@interface Person : NSObject
@property (nonatomic,strong)NSString *ID;
@property (nonatomic,strong)NSString *userId;
@property (nonatomic,strong)NSString *oldName;
@property (nonatomic,strong)Body *body;
@end

Copy the code
Key value conversion:

If the dictionary has a different naming style than the model, or multi-level mapping is required. The implementation of the following methods needs to be added to the implementation of the model class

+ (NSDictionary *)mj_replacedKeyFromPropertyName
{
    return @{
             @"ID" : @"id"The @"userId" : @"user_id"The @"oldName" : @"name.oldName"
             };
}
Copy the code

Note that attributes and keys are unaffected except for keys in the dictionary that are transformed accordingly.

Filtering rules
+ (NSArray *)mj_ignoredPropertyNames
{
    return@ [@"selected"];
}
Copy the code
2.2.1 principle

MJExtension mainly uses KVC and OC reflection mechanism

Dictionary model

When dictionary is transformed into model, its core is KVC, which mainly applies:

- (void)setValue:(nullable id)value forKey:(NSString *)key;
Copy the code

Before assigning a value to each attribute, it uses the Runtime function, using the OC reflection mechanism, to get the attribute name and type of all attributes. Filter all attribute names by whitelist and blacklist, and transform key values by inference of attribute types to ensure the safety and effectiveness of key values, and also deal with nested types accordingly. It also contains some caching policies.

The core code can be summarized as follows:

- (id)objectWithKeyValues:(NSDictionary *)keyValues type:(Class)type
{
    id obj = [[type alloc] init];
    unsigned int outCount = 0;
    objc_property_t *properties = class_copyPropertyList([Person class],&outCount);
    for(int i = 0; i < outCount; i++) { objc_property_t property = properties[i]; NSString *name = @(property_getName(property)); // Get member type NSString *attrs = @(property_getAttributes(property)); NSString *code = [self codeWithAttributes:attrs]; // Type conversion to class Class propertyClass = [self classWithCode:code]; id value = keyValues[name];if(! value || value == [NSNull null])continue;
        
        if(! [the self isFoundation: propertyClass] && propertyClass) {/ / model attribute value = [self objectWithKeyValues: valuetype:propertyClass];
        } else{ value = [self convertValue:value propertyClass:propertyClass code:code]; } // KVC sets the property value [obj]setValue:value forKey:name];
    }
    return obj;
}
Copy the code
Model to dictionary

The main process is to get the list of attribute names (keys) through reflection, then get the attribute value (value) through valueForKey, then filter the key and transform the value, and finally put all the key-value pairs into the dictionary to get the result.

Used in SWIFT

To be used in Swift, first the model class must inherit from NSObject, and all required properties must have the @objc modifier

Avoid using Bool (official documentation hints). However, in Swift5 and Xcode10, there is no other abnormality except that bool changes to 0 and 1 when it is converted into dictionary.)