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 conformCodable
Everything 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 theCodable
Types 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 compliance
Codable
In compliance withCodable
, we need to pay attention to:
- All attribute types must comply
Codable
. In addition to giving us our custom subclassesCodable
Additional 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 followed
Codable
If required include not followedCodable
How 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 pairs
Codable
Implementation of the protocol. We don’t care if it’s Codable for any calculated property - Not through extension
Codable
Type add complianceCodable
The 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 implementation
Encodable
andDecodable
Methods 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
- Proven, as long as manual implementation
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 realize
func 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.)