macOS 开发之 Swift 的 Codable

近日研究了一下如何将自定义类型数据持久化,在研究过程中发现 Swift 的 Codable 真的很方便,觉得有必要写一写这个 Codable,在本文一起探讨一下以下三个方面:

什么是 Swift 的 Codable

怎么使用 Codable

Codable 给我们带来什么便利

开发平台

macOS 10.14.4

Swift 5

xcode 10.2

1. Swift 的 Codable

In a nutshell, Encoding is the process of transforming your own custom type, class or struct to external data representation type like JSON or plist or something else & Decoding is the process of transforming external data representation type like JSON or plist to your own custom type, class or struct.

1.1 可以使用的协议

Swift 的标准库中包含了用于自定义类型(结构体、类)与其它表示形式(JSON、Property List 或 二进制)的数据之间相互转换的协议:

Encodable: 用于自定义类型向 JSON 或 Property List 的转换,协议包含一个方法

Swift encode(to:)

Decodable: 用于自 JSON 或 Property List 数据向自定义类型的转换,协议包含一个方法

Swift init(from:)

Codable: 包含 Encodable 和 Decodable 两方面的转换,其定义如下:

Swift typealias Codable = Decodable & Encodable

使用 Codable 协议 的 Encodable 和 Decodable,可以让我们轻松实现自定义数据类型的序列化以及得到相应数据类型的实例对象。按照字面意思,我们后面将数据类型实例向 JSON 或 Property List 转换的过程称为编码,反之,称为解码!

1.2 遵循协议的类型

如果我们想要实现自定义类型或数据模型的编码和解码,必须遵循 Codable 协议!Swift 基本的内建类型已经是 Codable 的了,比如 String、Int、Double、Date 和 Data。另外像 Array、Dictionary 和 Optional 也都是遵循 Codable 协议的,可以进行编码和解码。

如下自定义的结构体 Person 和 Team,遵循 Codable 协议,同时结构体的所有属性要么是标准的 Codable 类型,要么包含 Codable 类型:

structPerson:Codable{varid:Intvarname:Stringvarage:IntvarisMale:Bool}structTeam:Codable{varmaster:Personvarmemebers:[Person]}

1.3 Codable 类型的编解码

对于 Codable 的类型,需要使用相应的编码器对我们的数据进行编码和解码,具体细节可以参考后面的小节,有两种编码器可用:

JSON

JSONEncoder: 将 Codable 类型数据编码为 JSON 数据

JSONDecoder: 将 JSON 数据解码为指定的 Codable 类型数据

Property List

PropertyListEncoder: 将 Codable 类型数据编码为 plist 数据

PropertyListDecoder: 将 plist 数据解码为指定的 Codable 类型数据

2. Codable 的使用

下面我们就一起研究一下 Codable 的使用,这里我们只尝试 Codable 类型数据与 JSON 数据的编码解码的实现。

2.1 编码和解码

2.1.1 使用 JSONEncoder 编码

非常简单,只需调用 JSONEncoder 的 encode(_:) 方法就能将 Codable 类型转换为 JSON 数据:

letjack=Person(id:1,name:"Jack",age:12,isMale:true)ifletjackData=try?JSONEncoder().encode(jack){print(String(data:jackData,encoding:.utf8)!)}

可以看到打印出转换得到的 JSON 字符串数据:

{"age":12,"id":1,"isMale":true,"name":"Jack"}

是不是很方便,解码同样如此简单!

2.1.2 使用 JSONDecoder 解码

只需要调用 JSONDecoder 实例的 decode(_:from:) 方法就能将 JSON 对象转换得到指定类型的实例。

letjsonString="""{    "id": 2,    "name": "lucy",    "age": 11,    "isMale": false}"""ifletjson=jsonString.data(using:.utf8){letlucy=try?JSONDecoder().decode(Person.self,from:json)print(lucy!)}

打印结果,如您所料,将 JSON 字符串成功的解码为了 Person 实例对象:

Person(id:2,name:"lucy",age:11,isMale:false)

2.1.3 Codable 使用的基本步骤

综上过程,我们可以总结一下,要使用 Swift 的特性其实很简单,分两步:

自定义类型遵循 Codable 协议

使用编码器实现自定义类型数据的编码和解码

2.2 使用 CodingKeys

也许此时此刻您可能会想:

如果不想把自定义数据所有的属性编码到 JSON 该怎么办?

如果 JSON 数据中的键名与自定义类型中的属性名不一致怎么办?

请放心,您想到的,Apple 同样照顾到了!就是本节要讲的 CodingKeys

Codable types can declare a special nested enumeration named CodingKeys that conforms to the CodingKey protocol. When this enumeration is present, its cases serve as the authoritative list of properties that must be included when instances of a codable type are encoded or decoded. The names of the enumeration cases should match the names you've given to the corresponding properties in your type.

CodingKeys 是我们在自定义数据类型中定义的枚举,有以下两点要求:

枚举元素类型是 String,并且遵循 CodingKey 协议、

枚举元素的名称必须与自定义类型中的属性名称保持一致

那么我们回过头来看一下前面的两个问题怎么解决:

CodingKeys 中的元素与自定义数据类型中的属性名称对应,只要删除对应属性的枚举元素就可以实现编码时对应属性的忽略,这样就解决了第一个问题,但是要注意,不编码的属性必须赋予默认值

为 CodingKeys 中枚举元素自定义 String 值与 JSON 数据中的键名对应起来,就能解决第二个问题。

假如想为 Person 结构体添加一个 description 的属性,同时不想让它参与编码和解码,另外 JSON 数据中的键名是中文的,我们可以重构 Person 类:

structPerson:Codable{varid:Intvarname:Stringvarage:IntvarisMale:Boolvardescription:String="person"enumCodingKeys:String,CodingKey{caseid="身份证号"casename="姓名"caseage="年龄"caseisMale}}

我们看一下将自定义数据转换为 JSON 会是怎样的:

lettim=Person(id:3,name:"tim",age:10,isMale:true,description:"")iflettimData=try?JSONEncoder().encode(tim){print(String(data:timData,encoding:.utf8)!)}

编码得到的 JSON 数据如下:

{"姓名":"tim","isMale":true,"年龄":10,"身份证号":3}

2.3 自定义 encode 和 decode

我们定义一个 Size 结构体、Point 结构体和 Rect 结构体如下:

structSize:Codable{varwidth:Doublevarheight:Double}structPoint:Codable{varx:Doublevary:Double}structRect:Codable{varposition:Pointvarsize:Size}

我们利用一开始定义的 Rect 结构体声明一个 rect,坐标在原点,宽高都为 2.0,并将其转换为 JSON 数据:

letrect=Rect(position:Point(x:0.0,y:0.0),size:Size(width:2.0,height:2.0))ifletrectData=try?JSONEncoder().encode(rect){print(String(data:rectData,encoding:.utf8)!)}

team 对应的 JSON 字符串如下:

{"position":{"x":0,"y":0},"size":{"width":2,"height":2}}

但是呢,十里不想让 JSON 数据中 x 和 y 嵌套在 positon 中,也不想 width 和 height 嵌套在 size 中,而是像下面的样子:

{"x":0,"y":0,"width":2.0,"height":2.0}

要实现这种需求,我们必须自定义 Encodable 协议的 encode(_:) 方法 和 Decodable 协议的 init(from:) 方法,实现自定义的编码解码逻辑,大体分下面几步:

定义 CodingKeys 枚举,元素与目标 JSON 数据的键名对应,定义 x 和 y 而不是 position,定义 width 和 height 而不是 size

删除 Rect 定义中的 Codable

扩展 Rect 遵循 Encodable 协议,并实现 encode(_:) 方法

扩展 Rect 遵循 Decodable 协议,并实现 init(from:) 方法

最终 Rect 定义如下:

structRect{varposition:Pointvarsize:SizeenumCodingKeys:String,CodingKey{casexcaseycasewidthcaseheight}}extensionRect:Encodable{funcencode(toencoder:Encoder)throws{varcontainer=encoder.container(keyedBy:CodingKeys.self)trycontainer.encode(position.x,forKey:.x)trycontainer.encode(position.y,forKey:.y)trycontainer.encode(size.width,forKey:.width)trycontainer.encode(size.height,forKey:.height)}}extensionRect:Decodable{init(fromdecoder:Decoder)throws{letcontainer=trydecoder.container(keyedBy:CodingKeys.self)letx=trycontainer.decode(Double.self,forKey:.x)lety=trycontainer.decode(Double.self,forKey:.y)position=Point(x:x,y:y)letwidth=trycontainer.decode(Double.self,forKey:.width)letheight=trycontainer.decode(Double.self,forKey:.height)size=Size(width:width,height:height)}}

测试一下编码实现:

letrect=Rect(position:Point(x:0.0,y:0.0),size:Size(width:2.0,height:2.0))ifletrectData=try?JSONEncoder().encode(rect){print(String(data:rectData,encoding:.utf8)!)}

得到的 JSON 数据的打印结果为:

{"y":0,"x":0,"width":2,"height":2}

测试一下解码的实现:

letrectString="""{    "x": 3,    "y": 3,    "width": 2.5,    "height": 2.5}"""ifletjson=rectString.data(using:.utf8){letnewRect=try?JSONDecoder().decode(Rect.self,from:json)print(newRect!)}

得到的 newRect 实例对象打印结果:

Rect(position:__lldb_expr_21.Point(x:3.0,y:3.0),size:__lldb_expr_21.Size(width:2.5,height:2.5))

3. Codable 带来的福利

上面说了这么多的 Codable ,到底我们能用它来干什么呢,最主要的两个应用方向就是自定义类型数据持久化和网络通信。

3.1 数据持久化

其中一种常用的数据持久化方式就是属性列表(Property List),UserDefaults.standard 适合存储轻量级的本地数据,其提供了与默认数据库相交互的编程接口。其实它存储在应用程序的一个plist文件里,路径为应用沙盒Document目录平级的 /Library/Prefereces 里。另外,其只能存储可以序列化的数据类型,比如我们一开始说的那些基本类型,也就是 Codable 的,所以一旦我们自定义的数据类型遵循了 Codable 协议,即可序列化了,那我们的自定义类型的数据可以自由存取了。

注意:

使用这种方式一般用于存储应用程序的配置信息

手动调用 synchronize 方法可以立马将数据持久化存储

这里只是以这种数据持久化为例,讲一下如何持久化自定义数据类型的数据:

letrectDemo=Rect(position:Point(x:1.0,y:1.0),size:Size(width:2.0,height:2.0))ifletdemoData=try?JSONEncoder().encode(rectDemo){UserDefaults.standard.set(demoData,forKey:"rect-test")UserDefaults.standard.synchronize()print("saved successfully!")}ifletrectJson=UserDefaults.standard.value(forKey:"rect-test")as?Data{letnewRect=try?JSONDecoder().decode(Rect.self,from:rectJson)print(newRect!)}

可以看到打印结果:

savedsuccessfully!Rect(position:__lldb_expr_29.Point(x:1.0,y:1.0),size:__lldb_expr_29.Size(width:2.0,height:2.0))

3.2 网络通信

HTTP/HTTPS 网络通信中 JSON 是常用的交互数据类型,假如我们编写一个网络接口,当外部请求的时候,我们为其返回一个响应,如果我们定义一个 Codable 的 Response 类型,可以方便生成响应数据:

structResponse:Codable{varststus:Intvarmessage:String}letres=Response(ststus:200,message:"OK")letresData=try?JSONEncoder().encode(res)

总结

Swift 4 开始支持的 Codable 大大简化了对自定义类型数据序列化的实现,相信您会用得到!

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

推荐阅读更多精彩内容