前言
本篇是Swift4中Codable的使用系列第一篇,通過(guò)本篇文章我們來(lái)了解Codable的基本用法决侈。
自Swift4發(fā)布以來(lái)已有一段時(shí)間了,各種新特性為我們提供更加高效的開(kāi)發(fā)效率,其中在Swift4中使用Codable協(xié)議進(jìn)行模型與json數(shù)據(jù)之間的映射提供更加便利的方式干像。在Swift3中,對(duì)于從服務(wù)器獲取到的json數(shù)據(jù)后,我們要進(jìn)行一系列繁瑣的操作才能將數(shù)據(jù)完整的轉(zhuǎn)化成模型麻汰,舉個(gè)??速客,我們從服務(wù)器獲取了一段這樣的json數(shù)據(jù):
{
"student": {
"name": "Jone",
"age": 18,
"finger": {
"count": 10
}
}
}
然后我們用JSONSerialization
來(lái)解析數(shù)據(jù),得到的是一個(gè)Any
類型五鲫。當(dāng)我們要讀取count
時(shí)就要采取以下操作:
let json = try! JSONSerialization.jsonObject(with: data, options: [])
if let jsonDic = json as? [String:Any] {
if let student = jsonDic["student"] as? [String:Any] {
if let finger = student["finger"] as? [String:Int] {
if let count = finger["count"] {
print(count)
}
}
}
}
難過(guò)不難過(guò)...在日常用Swift編寫(xiě)代碼時(shí)溺职,就我而言,我喜歡使用SwiftyJSON或則ObjectMapper來(lái)進(jìn)行json轉(zhuǎn)模型位喂,因?yàn)橄啾仍母ㄔ福褂眠@些第三方會(huì)給我們帶來(lái)更高的效率。于是在Swift4中忆某,Apple官方就此提供了自己的方法点待,現(xiàn)在我們來(lái)了解其基本的用法。
JSONSerialization
:(iOS之蘋(píng)果自帶的json
解析NSJSONSerialization
(序列化))
NSJSONSerialization
提供了將JSON
數(shù)據(jù)轉(zhuǎn)換為Foundation
對(duì)象(一般都是NSDictionary
和NSArray
)和Foundation
對(duì)象轉(zhuǎn)換為JSON
數(shù)據(jù)(可以通過(guò)調(diào)用isValidJSONObject
來(lái)判斷Foundation
對(duì)象是否可以轉(zhuǎn)換為JSON
數(shù)據(jù))
Codable的簡(jiǎn)單使用
首先弃舒,我們來(lái)對(duì)最簡(jiǎn)單的json數(shù)據(jù)進(jìn)行轉(zhuǎn)模型癞埠,現(xiàn)在我們有以下一組json數(shù)據(jù):
let res = """
{
"name": "Jone",
"age": 17
}
"""
let data = res.data(using: .utf8)!
然后我們定義一個(gè)Student
結(jié)構(gòu)體作為數(shù)據(jù)的模型,并遵守Codable
協(xié)議:
struct Student: Codable {
let name: String
let age: Int
}
而關(guān)于Codable
協(xié)議的描述我們可以點(diǎn)進(jìn)去看一下:
public typealias Codable = Decodable & Encodable
public protocol Encodable {
public func encode(to encoder: Encoder) throws
}
public protocol Decodable {
public init(from decoder: Decoder) throws
}
其實(shí)就是遵守一個(gè)關(guān)于解碼的協(xié)議和一個(gè)關(guān)于編碼的協(xié)議聋呢,只要遵守這些協(xié)議才能進(jìn)行json與模型之間的編碼與解碼苗踪。
接下來(lái)我們就可以進(jìn)行講json解碼并映射成模型:
let decoder = JSONDecoder()
let stu = try! decoder.decode(Student.self, from: data)
print(stu) //Student(name: "Jone", age: 17)
然后,我們可以將剛才得到的模型進(jìn)行編碼成json:
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted //輸出格式好看點(diǎn)
let data = try! encoder.encode(stu)
print(String(data: data, encoding: .utf8)!)
//{
// "name" : "Jone",
// "age" : 17
//}
就是這么簡(jiǎn)單~~~
這里對(duì)encode和decode使用try!是為了減少文章篇幅削锰,正常使用時(shí)要對(duì)錯(cuò)誤進(jìn)行處理通铲,而常見(jiàn)的錯(cuò)誤會(huì)在第三篇講到。
json中的key和模型中的Key不對(duì)應(yīng)
通常器贩,我們從服務(wù)器獲取到的json里面的key命名方式和我們是有區(qū)別的颅夺,后臺(tái)對(duì)一些key的命名方式喜歡用下劃線來(lái)分割單詞,而我們更習(xí)慣于使用駝峰命名法蛹稍,例如這樣的情況:
let res = """
{
"name": "Jone",
"age": 17,
"born_in": "China"
}
"""
let data = res.data(using: .utf8)!
struct Student: Codable {
let name: String
let age: Int
let bornIn: String
}
顯然吧黄,在映射成模型的過(guò)程中會(huì)因?yàn)閖son中key與屬性名稱對(duì)不上而報(bào)錯(cuò),而此時(shí)我們就可以使用CodingKey
這個(gè)protocol
來(lái)規(guī)確定屬性名和json
中的key
的映射規(guī)則唆姐,我們現(xiàn)在看看這個(gè)協(xié)議:
public protocol CodingKey {
public var stringValue: String { get }
public init?(stringValue: String)
public var intValue: Int? { get }
public init?(intValue: Int)
}
而實(shí)現(xiàn)這個(gè)功能最簡(jiǎn)單的方式是使用一個(gè)enum
來(lái)遵守這個(gè)協(xié)議并且會(huì)自動(dòng)實(shí)現(xiàn)這個(gè)protocol
里的方法拗慨,使用case
來(lái)指定映射規(guī)則:
struct Student: Codable {
let name: String
let age: Int
let bornIn: String
enum CodingKeys: String, CodingKey {
case name
case age
case bornIn = "born_in"
}
}
現(xiàn)在就能很好的工作了
let decoder = JSONDecoder()
let stu = try! decoder.decode(Student.self, from: json)
print(stu) //Student(name: "Jone", age: 17, bornIn: "China")
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted //輸出格式好看點(diǎn)
let data = try! encoder.encode(stu)
print(String(data: data, encoding: .utf8)!)
//{
// "name" : "Jone",
// "age" : 17,
// "born_in" : "China"
//}
處理JSON中的日期格式,浮點(diǎn)數(shù)奉芦,base64編碼赵抢,URL
日期格式
現(xiàn)在我們就上個(gè)模型做一些簡(jiǎn)化,并添加一個(gè)新的屬性用于表示入學(xué)的注冊(cè)時(shí)間:
struct Student: Codable {
let registerTime: Date
enum CodingKeys: String, CodingKey {
case registerTime = "register_time"
}
}
let stu = Student(registerTime: Date())
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let encodedData = try encoder.encode(stu)
print(String(data: encodedData, encoding: .utf8)!)
//{
// "register_time" : 532248572.46527803
//}
如果我們不想時(shí)間以浮點(diǎn)數(shù)的形式來(lái)出現(xiàn)声功,我們可以對(duì)encoder的dateEncodingStrategy屬性進(jìn)行一些設(shè)置:
encoder.dateEncodingStrategy = .iso8601
// "register_time" : "2017-11-13T06:48:40Z"
let formatter = DateFormatter()
formatter.dateFormat = "MMM-dd-yyyy HH:mm:ss zzz"
encoder.dateEncodingStrategy = .formatted(formatter)
// "register_time" : "Nov-13-2017 14:55:30 GMT+8"
浮點(diǎn)數(shù)
有時(shí)服務(wù)器返回一個(gè)數(shù)據(jù)是一些特殊值時(shí)烦却,例如返回的學(xué)生高度的數(shù)值是一個(gè)NaN
,這時(shí)我們對(duì)decoder
的nonConformingFloatDecodingStrategy
屬性進(jìn)行設(shè)置:
struct Student: Codable {
let height: Float
}
let res = """
{
"height": "NaN"
}
"""
let json = res.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.nonConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: "+∞", negativeInfinity: "-∞", nan: "NaN")
print((try! decoder.decode(Student.self, from: json)).height) //nan
base64編碼
有時(shí)服務(wù)器返回一個(gè)base64
編碼的數(shù)據(jù)時(shí)减噪,我們對(duì)decoder
的dataDecodingStrategy
屬性進(jìn)行設(shè)置:
struct Student: Codable {
let blog: Data
}
let res = """
{
"blog": "aHR0cDovL3d3dy5qaWFuc2h1LmNvbS91c2Vycy8zMjhmNWY5ZDBiNTgvdGltZWxpbmU="
}
"""
let json = res.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64
let stu = try! decoder.decode(Student.self, from: json)
print(String(data: stu.blog, encoding: .utf8)!)
// http://www.reibang.com/users/328f5f9d0b58/timeline
URL
而對(duì)于URL來(lái)說(shuō)短绸,直接映射就可以了
struct Student: Codable {
let blogUrl: URL
}
let res = """
{
"blogUrl": "http://www.reibang.com/users/328f5f9d0b58/timeline"
}
"""
let json = res.data(using: .utf8)!
let decoder = JSONDecoder()
print(try! decoder.decode(Student.self, from: json).blogUrl)
// http://www.reibang.com/users/328f5f9d0b58/timeline
處理常見(jiàn)的JSON嵌套結(jié)構(gòu)
在此之前,因?yàn)樵趈son和模型之間轉(zhuǎn)換的過(guò)程是類似的筹裕,為了節(jié)約時(shí)間醋闭,先定義兩個(gè)泛型函數(shù)用于encode
和decode:
func encode<T>(of model: T) throws where T: Codable {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let encodedData = try encoder.encode(model)
print(String(data: encodedData, encoding: .utf8)!)
}
func decode<T>(of jsonString: String, type: T.Type) throws -> T where T: Codable {
let data = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
let model = try! decoder.decode(T.self, from: data)
return model
}
用對(duì)象封裝數(shù)組
對(duì)于使用一個(gè)對(duì)象來(lái)封裝了一數(shù)組的json:
let res = """
{
"students" : [
{
"name": "ZhangSan",
"age": 17,
"sex": "male",
"born_in": "China"
},
{
"name": "LiSi",
"age": 18,
"sex": "male",
"born_in": "Japan"
},
{
"name": "WangWu",
"age": 16,
"sex": "male",
"born_in": "USA"
}
]
}
"""
對(duì)于這類json,我們只需要定義一個(gè)類型朝卒,該類型包含一個(gè)數(shù)組证逻,數(shù)組類型就是這些內(nèi)嵌類型
struct Classes: Codable {
let students: [Student]
struct Student: Codable {
let name: String
let age: Int
let sex: SexType
let bornIn: String
enum SexType: String, Codable {
case male
case female
}
enum CodingKeys: String, CodingKey {
case name
case age
case sex
case bornIn = "born_in"
}
}
}
let c = try! decode(of: res, type: Classes.self)
dump(c)
try! encode(of: c)
數(shù)組作為JSON根對(duì)象
如果服務(wù)器返回來(lái)的數(shù)據(jù)如果是一個(gè)數(shù)組,而數(shù)組里面的是一個(gè)個(gè)對(duì)象的字典:
let res = """
[
{
"name": "ZhangSan",
"age": 17,
"sex": "male",
"born_in": "China"
},
{
"name": "LiSi",
"age": 18,
"sex": "male",
"born_in": "Japan"
},
{
"name": "WangWu",
"age": 16,
"sex": "male",
"born_in": "USA"
}
]
"""
對(duì)于這種類型抗斤,我們也將它轉(zhuǎn)化了一個(gè)數(shù)組囚企,數(shù)組的類型就是json數(shù)組中字典所代表的對(duì)象類型
struct Student: Codable {
let name: String
let age: Int
let sex: SexType
let bornIn: String
enum SexType: String, Codable {
case male
case female
}
enum CodingKeys: String, CodingKey {
case name
case age
case sex
case bornIn = "born_in"
}
}
let stu = try! decode(of: res, type: [Student].self)
dump(stu)
try! encode(of: stu)
純數(shù)組中的對(duì)象帶有唯一Key
如果數(shù)據(jù)是由多個(gè)字典組成的數(shù)組,字典里又有一組鍵值對(duì)瑞眼,這種格式可以看成是前兩種的組合:
let res = """
[
{
"student": {
"name": "ZhangSan",
"age": 17,
"sex": "male",
"born_in": "China"
}
},
{
"student": {
"name": "LiSi",
"age": 18,
"sex": "male",
"born_in": "Japan"
}
},
{
"student": {
"name": "WangWu",
"age": 16,
"sex": "male",
"born_in": "USA"
}
}
]
"""
解析這種數(shù)據(jù)龙宏,我們像第二種方式一樣,對(duì)于外圍的數(shù)組我們只需要在內(nèi)層的類型中加上一個(gè)中括號(hào)就可以了伤疙,而里面的類型這里我們需要定義成Dictionary<String, Student>:
struct Student: Codable {
let name: String
let age: Int
let sex: SexType
let bornIn: String
enum SexType: String, Codable {
case male
case female
}
enum CodingKeys: String, CodingKey {
case name
case age
case sex
case bornIn = "born_in"
}
}
let stu = try! decode(of: res, type: [Dictionary<String, Student>].self)
dump(stu)
try! encode(of: stu)
更一般的復(fù)雜情況
接下來(lái)我們看一種類型银酗,對(duì)于這種類型相對(duì)之前更復(fù)雜,但處理起來(lái)也是很簡(jiǎn)單徒像,日常開(kāi)發(fā)中也是接觸最多這種情況:
let res = """
{
"info": {
"grade": "3",
"classes": "1112"
},
"students" : [
{
"name": "ZhangSan",
"age": 17,
"sex": "male",
"born_in": "China"
},
{
"name": "LiSi",
"age": 18,
"sex": "male",
"born_in": "Japan"
},
{
"name": "WangWu",
"age": 16,
"sex": "male",
"born_in": "USA"
}
]
}
"""
我們按照老套路一個(gè)一個(gè)來(lái)定制模型其實(shí)也是很簡(jiǎn)單的:
struct Response: Codable {
let info: Info
let students: [Student]
struct Info: Codable {
let grade: String
let classes: String
}
struct Student: Codable {
let name: String
let age: Int
let sex: SexType
let bornIn: String
enum SexType: String, Codable {
case male
case female
}
enum CodingKeys: String, CodingKey {
case name
case age
case sex
case bornIn = "born_in"
}
}
}
let response = try! decode(of: res, type: Response.self)
dump(response)
try! encode(of: response)
Swift 4.1 / 2018.04.03
apple在3.29更新了Swift 4.1黍特,其中對(duì)Codable
進(jìn)行了優(yōu)化。在文章上面的內(nèi)容中锯蛀,我們使用CodingKeys
的enum
來(lái)將屬性名與json不一致的key來(lái)進(jìn)行映射灭衷。不過(guò)大家是否有發(fā)現(xiàn),不一致是因?yàn)閖son的key是用下劃線分割單詞旁涤,而我們的屬性命名是用駝峰命名法翔曲。于是Swift4.1中我們可以為JSONEncoder
的keyEncodingStrategy
屬性設(shè)置為convertToSnakeCase
和JSONDecoder
的keyDecodingStrategy
屬性設(shè)置為convertFromSnakeCase
。這樣我們就不用寫(xiě)CodingKeys
來(lái)處理這個(gè)問(wèn)題了劈愚。
struct Person: Codable {
let name: String
let age: Int
let bornIn: String
}
let ming = Person(name: "ming", age: 18, bornIn: "China")
let alex = Person(name: "alex", age: 22, bornIn: "USA")
let people1 = [ming, alex]
let encoder = JSONEncoder()
// 設(shè)置為 convertToSnakeCase
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.outputFormatting = .prettyPrinted
let jsonData = try! encoder.encode(people1)
if let jsonstr = String(data: jsonData, encoding: .utf8) {
print(jsonstr)
/** 將駝峰命名法變成了用下劃線分割
[
{
"name" : "ming",
"age" : 18,
"born_in" : "China"
},
{
"name" : "alex",
"age" : 22,
"born_in" : "USA"
}
]
*/
}
let decoder = JSONDecoder()
// 設(shè)置為 convertFromSnakeCase
decoder.keyDecodingStrategy = .convertFromSnakeCase
let people2 = try! decoder.decode([Person].self, from: jsonData)
dump(people2)
/** 將用下劃線分割的key映射為用駝峰命名法命名的屬性
? 2 elements
? Swift_4_1_Codable.Person
- name: "ming"
- age: 18
- bornIn: "China"
? Swift_4_1_Codable.Person
- name: "alex"
- age: 22
- bornIn: "USA"
*/
在日常開(kāi)發(fā)中部默,后端傳過(guò)來(lái)json中的key大部分是用下劃線分割的,因此在Swift4的時(shí)候我們使用Codable
時(shí)候還要寫(xiě)那繁瑣的CodingKeys
造虎,到了Swift4.1傅蹂,我們就不用寫(xiě)了??。
至此算凿,我們對(duì)
Codable
的基本使用已經(jīng)熟悉了份蝴,只要遵守了Codable
協(xié)議就能享受其帶來(lái)的編碼與解碼的便利,若我們需要制定key和屬性名的對(duì)應(yīng)規(guī)則我們就需要使用CodingKey
協(xié)議氓轰,對(duì)于日常開(kāi)發(fā)中能滿足我們大部分的需求婚夫,但也只是大部分,因?yàn)檫€有時(shí)候我們需要對(duì)數(shù)據(jù)進(jìn)行一些處理署鸡,這是我們就需要自定義其編碼與解碼的過(guò)程了案糙,下一篇我將介紹更多Codable
協(xié)議的內(nèi)容限嫌。
Swift JSON編碼時(shí)支持Camel Case和Snake Case之間的轉(zhuǎn)換
Swift 4.0引入了Codable,但是有個(gè)麻煩的問(wèn)題:如果JSON數(shù)據(jù)的key命名格式是snake_case的話时捌,我們必須創(chuàng)建自己的CodingKeys來(lái)告訴蘋(píng)果怎么轉(zhuǎn)換怒医。
但是在Swift 4.1中,蘋(píng)果給JSONDecoder
引入了一個(gè)屬性keyDecodingStrategy
奢讨;對(duì)應(yīng)的JSONEncoder
引入了一個(gè)屬性keyEncodingStrategy
稚叹。這樣我們就不需要設(shè)置定義CodingKeys
了。只需要在decoding
的時(shí)候把keyDecodingStrategy
設(shè)置為.convertFromSnakeCase
拿诸;在encoding
的時(shí)候把keyEncodingStrategy
設(shè)置為.convertToSnakeCase
屬性名策略說(shuō)明:
CamelCase
策略扒袖,對(duì)象屬性:personId
,序列化后屬性:persionId
PascalCase
策略亩码,對(duì)象屬性:personId季率,序列化后屬性:PersonId
SnakeCase
策略,對(duì)象屬性:personId
描沟,序列化后屬性:person_id
KebabCase
策略蚀同,對(duì)象屬性:personId,序列化后屬性:person-id