爲了代碼可讀性以及開發效率,咱們每每會將數據抽象爲數據模型,在開發過程當中操做數據模型而不是數據自己。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外,無其餘異常)