Swift4中Codable的使用(一)

前言

本篇是Swift4中Codable的使用系列第一篇,通过本篇文章我们来了解Codable的基本用法。

自Swift4发布以来已有一段时间了,各种新特性为我们提供更加高效的开发效率,其中在Swift4中使用Codable协议进行模型与json数据之间的映射提供更加便利的方式。在Swift3中,对于从服务器获取到的json数据后,我们要进行一系列繁琐的操作才能将数据完整的转化成模型,举个🌰,我们从服务器获取了一段这样的json数据:

{
    "student": {
        "name": "Jone",
        "age": 18,
        "finger": {
            "count": 10
        }
    }
}

然后我们用JSONSerialization来解析数据,得到的是一个Any类型。当我们要读取count时就要采取以下操作:

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)
            }
        }
    }
}

难过不难过...在日常用Swift编写代码时,就我而言,我喜欢使用SwiftyJSON或则ObjectMapper来进行json转模型,因为相比原生的,使用这些第三方会给我们带来更高的效率。于是在Swift4中,Apple官方就此提供了自己的方法,现在我们来了解其基本的用法。


Codable的简单使用

首先,我们来对最简单的json数据进行转模型,现在我们有以下一组json数据:

let res = """
{
    "name": "Jone",
    "age": 17
}
"""
let data = res.data(using: .utf8)!

然后我们定义一个Student结构体作为数据的模型,并遵守Codable协议:

struct Student: Codable {
    let name: String
    let age: Int
}

而关于Codable协议的描述我们可以点进去看一下:

public typealias Codable = Decodable & Encodable

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

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

其实就是遵守一个关于解码的协议和一个关于编码的协议,只要遵守这些协议才能进行json与模型之间的编码与解码。
接下来我们就可以进行讲json解码并映射成模型:

let decoder = JSONDecoder()
let stu = try! decoder.decode(Student.self, from: data)
print(stu) //Student(name: "Jone", age: 17)

然后,我们可以将刚才得到的模型进行编码成json:

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted //输出格式好看点
let data = try! encoder.encode(stu)
print(String(data: data, encoding: .utf8)!)
//{
//    "name" : "Jone",
//    "age" : 17
//}

就是这么简单~~~

这里对encodedecode使用try!是为了减少文章篇幅,正常使用时要对错误进行处理,而常见的错误会在第三篇讲到。


json中的key和模型中的Key不对应

通常,我们从服务器获取到的json里面的key命名方式和我们是有区别的,后台对一些key的命名方式喜欢用下划线来分割单词,而我们更习惯于使用驼峰命名法,例如这样的情况:

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
}

显然,在映射成模型的过程中会因为json中key与属性名称对不上而报错,而此时我们就可以使用CodingKey这个protocol来规确定属性名和json中的key的映射规则,我们现在看看这个协议:

public protocol CodingKey {
    public var stringValue: String { get }
    public init?(stringValue: String)
    public var intValue: Int? { get }
    public init?(intValue: Int)
}

而实现这个功能最简单的方式是使用一个enum来遵守这个协议并且会自动实现这个protocol里的方法,使用case来指定映射规则:

struct Student: Codable {
    let name: String
    let age: Int
    let bornIn: String

    enum CodingKeys: String, CodingKey {
        case name
        case age
        case bornIn = "born_in"
    }
}

现在就能很好的工作了

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 //输出格式好看点
let data = try! encoder.encode(stu)
print(String(data: data, encoding: .utf8)!)
//{
//    "name" : "Jone",
//    "age" : 17,
//    "born_in" : "China"
//}

处理JSON中的日期格式,浮点数,base64编码,URL

日期格式

现在我们就上个模型做一些简化,并添加一个新的属性用于表示入学的注册时间:

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
//}

如果我们不想时间以浮点数的形式来出现,我们可以对encoder的dateEncodingStrategy属性进行一些设置:

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"

浮点数

有时服务器返回一个数据是一些特殊值时,例如返回的学生高度的数值是一个NaN,这时我们对decoder的nonConformingFloatDecodingStrategy属性进行设置:

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编码

有时服务器返回一个base64编码的数据时,我们队decoder的dataDecodingStrategy属性进行设置:

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.jianshu.com/users/328f5f9d0b58/timeline

URL

而对于URL来说,直接映射就可以了

struct Student: Codable {
    let blogUrl: URL
}
let res = """
{
    "blogUrl": "http://www.jianshu.com/users/328f5f9d0b58/timeline"
}
"""
let json = res.data(using: .utf8)!
let decoder = JSONDecoder()
print(try! decoder.decode(Student.self, from: json).blogUrl)
// http://www.jianshu.com/users/328f5f9d0b58/timeline

处理常见的JSON嵌套结构

在此之前,因为在json和模型之间转换的过程是类似的,为了节约时间,先定义两个泛型函数用于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
}

用对象封装数组

对于使用一个对象来封装了一数组的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"
        }
    ]
}
"""

对于这类json,我们只需要定义一个类型,该类型包含一个数组,数组类型就是这些内嵌类型

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)

数组作为JSON根对象

如果服务器返回来的数据如果是一个数组,而数组里面的是一个个对象的字典:

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"
    }
]
"""

对于这种类型,我们也将它转化了一个数组,数组的类型就是json数组中字典所代表的对象类型

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)

纯数组中的对象带有唯一Key

如果数据是由多个字典组成的数组,字典里又有一组键值对,这种格式可以看成是前两种的组合:

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"
        }
    }
]
"""

解析这种数据,我们像第二种方式一样,对于外围的数组我们只需要在内层的类型中加上一个中括号就可以了,而里面的类型这里我们需要定义成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)

更一般的复杂情况

接下来我们看一种类型,对于这种类型相对之前更复杂,但处理起来也是很简单,日常开发中也是接触最多这种情况:

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"
        }
    ]
}
"""

我们按照老套路一个一个来定制模型其实也是很简单的:

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,其中对Codable进行了优化。在文章上面的内容中,我们使用CodingKeysenum来将属性名与json不一致的key来进行映射。不过大家是否有发现,不一致是因为json的key是用下划线分割单词,而我们的属性命名是用驼峰命名法。于是Swift4.1中我们可以为JSONEncoderkeyEncodingStrategy属性设置为convertToSnakeCaseJSONDecoderkeyDecodingStrategy属性设置为convertFromSnakeCase。这样我们就不用写CodingKeys来处理这个问题了。

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()
// 设置为 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()
// 设置为 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"
 */

在日常开发中,后端传过来json中的key大部分是用下划线分割的,因此在Swift4的时候我们使用Codable时候还要写那繁琐的CodingKeys,到了Swift4.1,我们就不用写了👍。


至此,我们对Codable的基本使用已经熟悉了,只要遵守了Codable协议就能享受其带来的编码与解码的便利,若我们需要制定key和属性名的对应规则我们就需要使用CodingKey协议,对于日常开发中能满足我们大部分的需求,但也只是大部分,因为还有时候我们需要对数据进行一些处理,这是我们就需要自定义其编码与解码的过程了,下一篇我将介绍更多Codable协议的内容。

本文Demo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容