为了代码可读性以及开发效率,咱们每每会将数据抽象为数据模型,在开发过程当中操做数据模型而不是数据自己。php
在开发过程当中,咱们须要将key-value结构的数据,也就是字典,转化为数据模型。也就是字典转模型啦。git
字典转模型主要应用在两个场景。网络请求(json解析为模型、模型转字典做为请求参数),模型的数据持久化存取。github
下面咱们来分别探讨一下,OC跟swift中几种主流的字典转模型方式。json
Codable是swift4开始引入的类型,它包含两个协议Decodable
Encodable
swift
public typealias Codable = Decodable & Encodable
public protocol Encodable {
func encode(to encoder: Encoder) throws
}
public protocol Decodable {
init(from decoder: Decoder) throws
}
复制代码
使用Codable能够很方便的将数据解码为数据模型,将数据模型编码成数据。缓存
Codable
,一切将都变得很简单Encodable
跟Decodable
都有默认的实现。当咱们让模型听从Codable
后,就能够但是编解码了。安全
class User : Codable {
var name : String?
...
}
复制代码
json转模型bash
let model = try? JSONDecoder().decode(User.self, from: jsonData)
复制代码
模型转json网络
let tdata = try? JSONEncoder().encode(model)
复制代码
而一般状况下,咱们要处理的是字典转模型,将json解析步骤交给网络请求库。而Codable在实现json转模型过程当中,也是先解析为字典再进行模型转化的。数据结构
因此一般状况咱们的使用姿式是这样的:
// 字典转模型
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)
return nil
}
}
// 模型转字典
func 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
}
}
复制代码
Codable
当咱们的模型申明遵循Codable
协议时,常常会看到碰到does not conform to protocol
的报错,那么如何才能让模型顺利准从Codable
呢
Codable
的系统库中的类型:Codable
Codable
时,咱们须要注意:Codable
。除了给咱们咱们自定义的子类都加上Codable
申明外,其余类型必须是上述Codable类型之一Codable
Codable
属性如何处理在项目当中,咱们有时候须要在模型里申明一些非Codable
属性,譬如CLLocation、AVAsset等。接下来,咱们来探讨一下解决这类需求有那些可行方案
Codable
协议的实现。对于任何计算属性咱们不用关心它是否CodableCodable
类型添加遵循Codable
的申明
首先在extension中申明继承Encodable
协议,是支持的,除了class
类型必须声明为final
(Decodable
不支持)外。
可是它有个苛刻的条件,extension必须与类型申明在同一文件。
Implementation of 'Decodable' cannot be automatically synthesized in an extension in a different file to the type
- 这种方式显然是不可行的,除非咱们能够修改相应类型的源码
复制代码
实现CodingKeys,而且不包含非Codable属性。
若是CodingKeys中不包含非Codable类型的case,是能够经过编译的:
struct Destination : Codable {
var location : CLLocationCoordinate2D?
var city : String?
enum CodingKeys : String,CodingKey {
case city
}
}
复制代码
本身实现Encodable
和Decodable
并作相应转化
Encodable
和Decodable
协议中的方法,任何非Codable属性的申明都不会报错。而咱们须要作的是完成特定的数据结构与非Codable的类型属性的相互转化能够申明一个CodingKeys
枚举。
enum Codingkeys : String,CodingKey {
case id,name,age
case userId = "user_id"
}
复制代码
这样作了以后,在转化过程当中,会只处理全部已申明的case,而且根据原始值肯定key与属性的对应关系。
若是继承Codable
,系统默认会自动合成一个继承CodingKey
协议的枚举类型CodingKeys
,而且包含全部申明的属性值(不包含static变量),而且stringValue
与属性名称一致。
CodingKeys
的默认实现是private
的,只能在类内部使用
'CodingKeys' is inaccessible due to 'private' protection level
Decodable
跟Encodable
的默认实现都是基于CodingKeys
。若是咱们本身将其申明了,系统就会使用咱们申明的CodingKeys
而再也不自动生成。
值得注意的是:如过CodingKeys
中包含与属性中没有的case,会报错 does not conform to protocol
。
直接设置经过JSONDecoder
的设置就能够完成
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
复制代码
它能够讲全部带下划线分割符的key,转化为驼峰命名法
默认状况下能够将Double类型,也就是2001年1月1日到如今的时间戳(timeIntervalSinceReferenceDate),转化为Date类型。 咱们能够经过JSONDecoder
的dateDecodingStrategy
属性,来制定 Date 类型的解析策略
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
}
复制代码
作了上面这些已经能解决大部分业务场景,可是咱们须要在编解码过程当中对属性值进行转换,或者作多级映射,咱们须要实现Encodable``Decodable
两个协议方法,并对属性值作适当处理与转化
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)
}
}
复制代码
ObjectMapper是用swift编写的json转模型类库。它主要依赖Mappable
全部实现了这个协议的类型,均可以轻松实现json与模型之间的转化
首先在类中实现Mappable
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"]
}
}
复制代码
字典转模型
let u = UserMap(JSON:dict)
复制代码
模型转字典
let json = u?.toJSON()
复制代码
func mapping(map: Map)
方法,肯定属性与key映射关系,这一步骤会尤其繁琐,尤为是在属性值多时固然,有人将这个繁琐的过程自动化了
ObjectMapper-Plugin一个让当前类型代码自动添加Mappable
实现的插件
json4swift一个自动生成swift模型代码的网站。它是一个很好用的json转数据模型代码的工具。支持Codable、ObjectMapper、Classic Swift Key/Value Dictionary(经过硬编码方式,根据key-value经过属性的getter、setter方法赋值)
与Codable用法类似,让模型遵循HandyJSON
就能够进行字典转模型、json转模型了。
原理上,主要使用Reflection,依赖于从Swift Runtime源码中推断的内存规则
这是一个阿里的项目,感兴趣的同窗能够移步他的github
KVC全称是Key Value Coding,定义在NSKeyValueCoding.h文件中,是一个非正式协议。KVC提供了一种间接访问其属性方法或成员变量的机制,能够经过字符串来访问对应的属性方法或成员变量。
其核心方法是:
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKey:(NSString *)key;
复制代码
在字典转模型的方法,你们都很熟悉了:
Person *person = [[Person alloc] init];
[person setValuesForKeysWithDictionary:dic];
复制代码
它等同于
Person *p0 = [[Person alloc] init];
for (NSString *key in dic) {
[p0 setValue:dic[key] forKey:key];
}
复制代码
惟一不一样的是若是字典或json中存在null时,用setValue:forKey:
等到的是NSNull的属性值,而setValuesForKeysWithDictionary:
获得的是相应属性类型的nil值
模型转字典,能够用:
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
复制代码
在使用时,须要注意的是:
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{}
复制代码
对于嵌套类型kvc是不支持直接转化的,可是咱们可用经过重写相应属性的setter方法来实现
- (void)setBody:(Body *)body
{
if (![body isKindOfClass:[Body class]] && [body isKindOfClass:[NSDictionary class]]) {
_body = [[Body alloc] init];
[_body setValuesForKeysWithDictionary:(NSDictionary *)body];
}else{
_body = body;
}
}
复制代码
若是value要作必定转化时,也能够用相似方法
当字典中key值与属性名存在差别时,能够经过重写setValue:forUndefinedKey
实现
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
if ([key isEqualToString:@"id"]) {
self.ID = value;
}
}
复制代码
若是须要在swift中使用,首先模型类必须继承自NSObject
,全部须要的属性必须加上@objc
修饰符
MJExtension是OC中字典转模型最经常使用的第三方库。他的使用很简单。
// 字典转模型
User *user = [User mj_objectWithKeyValues:dict];
// 模型转字典
NSDictionary *userDict = user.mj_keyValues;
复制代码
而相较于直接使用kvc,它还还支持不少独有的特性
它支持下述这种嵌套模型。
@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
复制代码
若是字典与模型的命名风格不一致,或者须要多级映射。须要在模型类的实现中添加下列方法的实现
+ (NSDictionary *)mj_replacedKeyFromPropertyName
{
return @{
@"ID" : @"id",
@"userId" : @"user_id",
@"oldName" : @"name.oldName"
};
}
复制代码
值得注意的是,除了这个字典中的key作相应转化以外,其余属性和key是不受任何影响的。
+ (NSArray *)mj_ignoredPropertyNames
{
return @[@"selected"];
}
复制代码
MJExtension主要运用了KVC和OC反射机制
字典转模型时,其核心是KVC,主要运用:
- (void)setValue:(nullable id)value forKey:(NSString *)key;
复制代码
在为每个属性赋值以前,它使用runtime函数,运用oc反射机制,获取全部属性的属性名及类型。在对全部属性名进行白名单和黑名单的过滤,经过对属性类型推断,对键值进行相应转换,保证键值的安全有效,也对嵌套类型作相应处理。还包含一些缓存策略。
核心代码可概括以下:
- (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));
// 获取成员类型
NSString *attrs = @(property_getAttributes(property));
NSString *code = [self codeWithAttributes:attrs];
// 类型转换为class
Class propertyClass = [self classWithCode:code];
id value = keyValues[name];
if (!value || value == [NSNull null]) continue;
if (![self isFoundation:propertyClass] && propertyClass) {
// 模型属性
value = [self objectWithKeyValues:value type:propertyClass];
} else {
value = [self convertValue:value propertyClass:propertyClass code:code];
}
// kvc设置属性值
[obj setValue:value forKey:name];
}
return obj;
}
复制代码
主要流程是经过反射获取属性名称(key)的列表,再经过valueForKey:
获取属性值(value),而后对key进行过滤转换,value进行转换,最后把全部键值对放入字典中获得结果。
若是须要在swift中使用,首先模型类必须继承自NSObject
,全部须要的属性必须加上@objc
修饰符
避免使用Bool类型(官方文档的提示。而Swift五、Xcode10中实测,除了转成字典时bool变为0和1外,无其余异常)