Codable發(fā)布這么久我就不學,摸魚爽歪歪,哎~就是玩兒
寫在開頭
祝天下所有偉大的母親:
節(jié)日快樂,身體健康, 感謝您的付出,感謝您的仁愛!
----- 寫在母親節(jié)當天
前言
對于大多數(shù)的應用程序來說,最常見的任務就是進行網(wǎng)絡數(shù)據(jù)的發(fā)送和接收,但是在執(zhí)行此操作之前,我們需要通過編碼或者序列化的方式將數(shù)據(jù)轉(zhuǎn)換為合適的格式來發(fā)送,然后還需要將收到的網(wǎng)絡數(shù)據(jù)轉(zhuǎn)換為合適的格式,這樣才能在應用中使用它們,這樣的過程叫做解碼或著叫反序列化。
那如何去定義這個格式呢!這里就不得不提 JSON 了,JSON 目前是網(wǎng)絡通信發(fā)送和接收數(shù)據(jù)最常用的格式,但是在 Swift4.0 之前,大家都是用一些第三方的開源庫來對 JSON 格式進行解析。
終于, Apple 在 Swift4.0 的 Foundtion 模塊中添加了對 JSON 解析的原生支持,它的功能強大而且易于使用,接下來就讓我?guī)Т蠹?了解下在 swift 里如何來對你的數(shù)據(jù)進行 encoding 和 decoding 吧!
基礎知識介紹
在 swift 里要對 JSON 進行處理的話,首先需要了解的概念就是:Codable, Codable 其實它不是一個協(xié)議,而是另外倆個協(xié)議的組合:Decodable 和 Encodable,它的源碼如下所示:
public typealias Codable = Decodable & Encodable
所以聰明的你一定可以猜到,只要數(shù)據(jù)模型遵行了 Codable 協(xié)議,那么就可以方便的進行 JSON 和數(shù)據(jù)模型的相互轉(zhuǎn)換了。
在 Swift4.0 中,Apple 提供了 JSONEncoder 和 JSONDecoder 倆對象來處理 JSON 的編碼和解碼,核心代碼如下:
let encoder = JSONEncoder()
let decoder = JSONDecoder()
相關的概念已介紹完畢,你準備好迎接挑戰(zhàn)了嗎?

JSON 轉(zhuǎn)數(shù)據(jù)模型
TASK 1:簡單的數(shù)據(jù)結(jié)構(gòu)
如果你的 JSON 結(jié)構(gòu)和你使用的數(shù)據(jù)模型結(jié)構(gòu)一致的話,那么解析過程將會非常簡單,請看下面內(nèi)容:
下面給出的是一個歌曲的 JSON 數(shù)據(jù),我現(xiàn)在要將其轉(zhuǎn)換為 SongModel。
let song = """
{
"singer": "The Chainsmokers",
"name": "Something Just Like This",
}
"""
SongModel
/// SongModel模型,遵循 Codable 協(xié)議
struct SongModel: Codable {
var singer: String?
var name: String?
}
轉(zhuǎn)換過程如下:
if let jsonData = song.data(using: String.Encoding.utf8) {
if let sSong = try? JSONDecoder().decode(SongModel.self, from: jsonData) {
dump(sSong)
}
}
輸出結(jié)果如下:
? JSONDecoderDemo.SongModel
? singer: Optional("The Chainsmokers")
- some: "The Chainsmokers"
? name: Optional("Something Just Like This")
- some: "Something Just Like This"
這樣就完成了解析,是不是很簡單!
NOTE:在數(shù)據(jù)模型的成員變量中,基本數(shù)據(jù)類型如:String、Int、Float等都已經(jīng)實現(xiàn)了 Codable 協(xié)議,因此如果你的數(shù)據(jù)類型只包含這些基本數(shù)據(jù)類型的屬性,只需要在類型聲明中加上 Codable 協(xié)議就可以了,不需要寫任何實際實現(xiàn)的代碼。
TASK 2: 解析數(shù)組
假如這是我們收到的一張專輯 Album 的 JSON 數(shù)據(jù),現(xiàn)在要把它轉(zhuǎn)化成 AlbumModel 數(shù)據(jù)模型。
let album = """
{
"singer": "The Chainsmokers",
"name": "Something Just Like This",
"songs":[
"Something Just Like This",
"Closer",
"Young",
"All We Know"
]
}
"""
AlbumModel
struct AlbumModel: Codable {
var singer: String?
var name: String?
var songs: [String]?
}
轉(zhuǎn)換過程如下:
if let jsonData = album.data(using: String.Encoding.utf8) {
if let sSong = try? JSONDecoder().decode(AlbumModel.self, from: jsonData) {
dump(sSong)
}
}
輸出結(jié)果為:
? JSONDecoderDemo.AlbumModel
? singer: Optional("The Chainsmokers")
- some: "The Chainsmokers"
? name: Optional("Something Just Like This")
- some: "Something Just Like This"
? songs: Optional(["Something Just Like This", "Closer", "Young", "All We Know"])
? some: 4 elements
- "Something Just Like This"
- "Closer"
- "Young"
- "All We Know"
和上面的轉(zhuǎn)換如出一轍,想必你現(xiàn)在心里已經(jīng)在默默的嘀咕:這么簡單還用你講?

那接下來就請準備迎接新的挑戰(zhàn)把!
TASK 3:結(jié)構(gòu)不一致
上面所演示的 JSON 數(shù)據(jù)格式都是與數(shù)據(jù)模型里的成員變量一一對應的,但是,在實際開發(fā)中,你會經(jīng)常遇到數(shù)據(jù)源的格式和數(shù)據(jù)模型結(jié)構(gòu) 不一致的情況,很多情況下可能是服務端與客戶端沒有統(tǒng)一好接口的格式,然后各自就開始開發(fā),到需要進行調(diào)試的時候,客戶端一收到消息,就懵逼了:

NOTE: 所以在這里我非常建議大家在做功能開發(fā)之前一定要先把接口文檔定義好,定義好,定義好,重要的事情講三遍。
這樣服務端和客戶端之間定義的數(shù)據(jù)格式就存在了差異,無論怎樣當然總有一方需要作出讓步來做到兼容,那么當客戶端想要做兼容時,該怎么處理呢!我們先來看個例子:
例如服務端返回的數(shù)據(jù)為:
let album = """
{
"singer": "The Chainsmokers",
"name": "Something Just Like This",
"songList":[
"Something Just Like This",
"Closer",
"Young",
"All We Know"
]
}
"""
可以看到,原來的 songs 換成了 songList,我們執(zhí)行下原先的代碼,看下輸出:
? JSONDecoderDemo.AlbumModel
? singer: Optional("The Chainsmokers")
- some: "The Chainsmokers"
? name: Optional("Something Just Like This")
- some: "Something Just Like This"
- songs: nil
發(fā)現(xiàn) songs 字段變成了 nil, 這個解析就失敗了,那如何做到不修改我之前定義的數(shù)據(jù)模型的成員變量,來做到兼容呢!這時候就需要用到 CodingKey 協(xié)議了, 借助 CodingKey 可以用來映射數(shù)據(jù)模型的成員變量,首先在數(shù)據(jù)模型中添加一個特殊的枚舉類型:
private enum CodingKeys: String, CodingKey
添加完后的數(shù)據(jù)模型代碼如下:
struct AlbumModel: Codable {
var singer: String?
var name: String?
var songs: [String]?
enum CodingKeys: String, CodingKey {
case singer = "singer"
case name = "name"
case songs = "songList"
}
}
輸出結(jié)果為:
? JSONDecoderDemo.AlbumModel
? singer: Optional("The Chainsmokers")
- some: "The Chainsmokers"
? name: Optional("Something Just Like This")
- some: "Something Just Like This"
? songs: Optional(["Something Just Like This", "Closer", "Young", "All We Know"])
? some: 4 elements
- "Something Just Like This"
- "Closer"
- "Young"
- "All We Know"
這樣,我們是不是就可以正常解析 JSON 數(shù)據(jù)了,是不是很強大。
接下來再增加一個難度!
當給你的唱片的 JSON 結(jié)構(gòu)是這樣的,你該怎么解析呢!
let album = """
{
"singer": "The Chainsmokers",
"name": "Something Just Like This",
"songs": "Something Just Like This"
}
"""
根據(jù)上面給出的例子,你會發(fā)現(xiàn)它依然與你的數(shù)據(jù)模型不匹配,原來的 songs 字段不是數(shù)組格式了,那如何才能正常的解析到數(shù)據(jù)模型上去呢,這時候就需要我們自己來實現(xiàn)編碼解碼的邏輯了。
首先在 AlbumModel 中加入以下代碼:
struct AlbumModel: Codable {
var singer: String?
var name: String?
var songs: [String]?
// 1
enum CodingKeys: String, CodingKey {
case singer = "singer"
case name = "name"
case songs = "songs"
}
// 解碼: JSON 轉(zhuǎn) Model
init(from decoder: Decoder) throws {
// 2
let container = try decoder.container(keyedBy: CodingKeys.self)
// 3
singer = try container.decode(String.self, forKey: .singer)
name = try container.decode(String.self, forKey: .name)
let ss = try container.decode(String.self, forKey: .songs)
songs = [ss]
}
// 編碼: Model 轉(zhuǎn) JSON
func encode(to encoder: Encoder) throws {
// 4
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(singer, forKey: .singer)
try container.encode(name, forKey: .name)
try container.encode(songs, forKey: .songs)
}
}
解釋如下:
創(chuàng)建 CodingKeys 枚舉,用于映射 JSON 字段。 創(chuàng)建一個解碼器容器,來存儲 JSON 里的屬性。 使用適當?shù)念愋秃途幋a鍵從容器中提取歌手和專輯名和歌單,由于歌單是數(shù)組類型的,所以需要將提取到的歌轉(zhuǎn)換成數(shù)組。 創(chuàng)建 KeyedEncodingContainer 容器來對數(shù)據(jù)模型里的屬性進行編碼。
轉(zhuǎn)換過程如下:
if let jsonData = album.data(using: String.Encoding.utf8) {
if let aAlbum = try? JSONDecoder().decode(AlbumModel.self, from: jsonData) {
dump(aAlbum)
}
}
結(jié)果如下:
? JSONDecoderDemo.AlbumModel
? singer: Optional("The Chainsmokers")
- some: "The Chainsmokers"
? name: Optional("Something Just Like This")
- some: "Something Just Like This"
? songs: Optional(["Something Just Like This"])
? some: 1 element
- "Something Just Like This"
可以看到通過上面的代碼,已經(jīng)可以將 JSON 中原先的 String 轉(zhuǎn)換成數(shù)據(jù)模型中的數(shù)組類型了。
注意:如果需要借助 CodingKeys 解決字段不一致的情況,即使其他的屬性不需要映射,也必須將其包含在枚舉中,譬如:singer, name,否則會報錯。
TASK 4:復雜的嵌套
除了處理簡單的數(shù)據(jù)模型,Codable 還可以處理復雜的嵌套數(shù)據(jù)模型,首先解釋下什么是嵌套數(shù)據(jù)模型:
譬如我有個專門處理專輯的數(shù)據(jù)模型叫 AlbumModel,它里面內(nèi)嵌了 SongModel 的屬性,這就是嵌套。這里必須要說明的就是嵌套的數(shù)據(jù)模型以及嵌套的子模型都必須遵循 Codable 協(xié)議,下面我們舉個嵌套的數(shù)據(jù)模型的例子來說明一下:
/// 專輯模型
struct AlbumModel: Codable {
// 專輯名
var albumName: String?
// 發(fā)布時間
var releaseTime: String?
// 歌單(嵌套)
var songs: [SongModel]?
}
/// 歌曲模型
struct SongModel: Codable {
// 歌手(嵌套)
var singer: SingerModel?
// 歌曲
var name: String?
}
/// 歌手模型
struct SingerModel: Codable {
// 姓名
var name: String?
// 年齡
var age: Int?
}
JSON 數(shù)據(jù)結(jié)構(gòu)
let album = """
{
"albumName": "Something Just Like This",
"releaseTime": "2017-02-22",
"songs":[
{
"singer": {
"name":"The Chainsmokers",
"age": 30
},
"name": "Something Just Like This"
},
{
"singer": {
"name":"The Chainsmokers",
"age": 30
},
"name": "Closer"
},
{
"singer": {
"name":"The Chainsmokers",
"age": 30
},
"name": "Young"
},
{
"singer": {
"name":"The Chainsmokers",
"age": 30
},
"name": "All We Know"
}
]
}
"""
轉(zhuǎn)換過程如下:
if let jsonData = album.data(using: String.Encoding.utf8) {
if let aAlbum = try? JSONDecoder().decode(AlbumModel.self, from: jsonData) {
dump(aAlbum)
}
}
輸出結(jié)果為:
JSONDecoderDemo.AlbumModel
? albumName: Optional("Something Just Like This")
- some: "Something Just Like This"
? releaseTime: Optional("2017-02-22")
- some: "2017-02-22"
? songs: Optional([JSONDecoderDemo.SongModel(singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30))), name: Optional("Something Just Like This")), JSONDecoderDemo.SongModel(singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30))), name: Optional("Closer")), JSONDecoderDemo.SongModel(singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30))), name: Optional("Young")), JSONDecoderDemo.SongModel(singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30))), name: Optional("All We Know"))])
? some: 4 elements
? JSONDecoderDemo.SongModel
? singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30)))
? some: JSONDecoderDemo.SingerModel
? name: Optional("The Chainsmokers")
- some: "The Chainsmokers"
? age: Optional(30)
- some: 30
? name: Optional("Something Just Like This")
- some: "Something Just Like This"
? JSONDecoderDemo.SongModel
? singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30)))
? some: JSONDecoderDemo.SingerModel
? name: Optional("The Chainsmokers")
- some: "The Chainsmokers"
? age: Optional(30)
- some: 30
? name: Optional("Closer")
- some: "Closer"
? JSONDecoderDemo.SongModel
? singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30)))
? some: JSONDecoderDemo.SingerModel
? name: Optional("The Chainsmokers")
- some: "The Chainsmokers"
? age: Optional(30)
- some: 30
? name: Optional("Young")
- some: "Young"
? JSONDecoderDemo.SongModel
? singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30)))
? some: JSONDecoderDemo.SingerModel
? name: Optional("The Chainsmokers")
- some: "The Chainsmokers"
? age: Optional(30)
- some: 30
? name: Optional("All We Know")
- some: "All We Know"
這樣這個嵌套就被解決了,接下來再挑戰(zhàn)一個難度更大的,請看代碼:
let album = """
{
"albumName": "Something Just Like This",
"releaseTime": "2017-02-22",
"songs": {
"favorite":[
{
"singer": {
"name":"The Chainsmokers",
"age": 30
},
"name": "Something Just Like This"
},
{
"singer": {
"name":"The Chainsmokers",
"age": 30
},
"name": "Closer"
},
{
"singer": {
"name":"The Chainsmokers",
"age": 30
},
"name": "Young"
},
{
"singer": {
"name":"The Chainsmokers",
"age": 30
},
"name": "All We Know"
}
]
}
}
"""
可以看到,在歌單 Songs 中又嵌套了一個 favorite 字段,這個 JSON 結(jié)構(gòu)相比 AlbumModel 這個數(shù)據(jù)模型又加深了一層,那該如何解析呢!

這里我們就要用到 nestedContainer 來處理這種嵌套,首先在 AlbumModel 加入如下代碼:
/// 專輯模型
struct AlbumModel: Codable {
// 專輯名
var albumName: String?
// 發(fā)布時間
var releaseTime: String?
// 歌單
var songs: [SongModel]?
// 1
enum CodingKeys: String, CodingKey {
case albumName, releaseTime, songs
}
// 2
enum favoriteKeys: CodingKey {
case favorite
}
// 解碼: JSON 轉(zhuǎn) Model
init(from decoder: Decoder) throws {
// 3
let container = try decoder.container(keyedBy: CodingKeys.self)
albumName = try container.decode(String.self, forKey: .albumName)
releaseTime = try container.decode(String.self, forKey: .releaseTime)
// 4
let favoriteContainer = try container.nestedContainer(keyedBy: favoriteKeys.self, forKey: .songs)
songs = try favoriteContainer.decode([SongModel].self, forKey: .favorite)
}
// 編碼: Model 轉(zhuǎn) JSON
func encode(to encoder: Encoder) throws {
// 5
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(albumName, forKey: .albumName)
try container.encode(releaseTime, forKey: .releaseTime)
// 6
var favoriteContainer = container.nestedContainer(keyedBy: favoriteKeys.self, forKey: .songs)
try favoriteContainer.encode(songs, forKey: .favorite)
}
}
/// 歌曲模型
struct SongModel: Codable {
// 歌手
var singer: SingerModel?
// 歌曲
var name: String?
}
/// 歌手模型
struct SingerModel: Codable {
// 姓名
var name: String?
// 年齡
var age: Int?
}
解析如下:
首先創(chuàng)建最頂層的 CodingKeys 創(chuàng)建嵌套層的 CodingKeys 創(chuàng)建頂層 CodingKeys 對應的容器,并對其解碼 創(chuàng)建嵌套層的容器,并對 favorite 解碼 創(chuàng)建編碼容器,并對 albumName 和 releaseTime 編碼 獲取嵌套容器,并對 favorite 編碼
轉(zhuǎn)換過程:
if let jsonData = album.data(using: String.Encoding.utf8) {
if let aAlbum = try? JSONDecoder().decode(AlbumModel.self, from: jsonData) {
dump(aAlbum)
}
}
輸出如下:
? JSONDecoderDemo.AlbumModel
? albumName: Optional("Something Just Like This")
- some: "Something Just Like This"
? releaseTime: Optional("2017-02-22")
- some: "2017-02-22"
? songs: Optional([JSONDecoderDemo.SongModel(singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30))), name: Optional("Something Just Like This")), JSONDecoderDemo.SongModel(singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30))), name: Optional("Closer")), JSONDecoderDemo.SongModel(singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30))), name: Optional("Young")), JSONDecoderDemo.SongModel(singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30))), name: Optional("All We Know"))])
? some: 4 elements
? JSONDecoderDemo.SongModel
? singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30)))
? some: JSONDecoderDemo.SingerModel
? name: Optional("The Chainsmokers")
- some: "The Chainsmokers"
? age: Optional(30)
- some: 30
? name: Optional("Something Just Like This")
- some: "Something Just Like This"
? JSONDecoderDemo.SongModel
? singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30)))
? some: JSONDecoderDemo.SingerModel
? name: Optional("The Chainsmokers")
- some: "The Chainsmokers"
? age: Optional(30)
- some: 30
? name: Optional("Closer")
- some: "Closer"
? JSONDecoderDemo.SongModel
? singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30)))
? some: JSONDecoderDemo.SingerModel
? name: Optional("The Chainsmokers")
- some: "The Chainsmokers"
? age: Optional(30)
- some: 30
? name: Optional("Young")
- some: "Young"
? JSONDecoderDemo.SongModel
? singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30)))
? some: JSONDecoderDemo.SingerModel
? name: Optional("The Chainsmokers")
- some: "The Chainsmokers"
? age: Optional(30)
- some: 30
? name: Optional("All We Know")
- some: "All We Know"
挑戰(zhàn)成功,看到這里是不是已經(jīng)有點暈了,說實話其實我自己也不知道我在表達啥,我也暈了,哈哈!

Task 6:處理派生類
下面我們來看下一個特殊的數(shù)據(jù)模型結(jié)構(gòu),它應該怎么去轉(zhuǎn)換呢!
當一個類遵循了 Codable 協(xié)議,那么它自身是可以很方便的使用 JSONEncoder 和 JSONDecoder 來 JSON 化和反 JSON 化的,但是如果有別的類繼承了它,那么對該子類的 JSON 化和反 JSON 化就不是那么方便了。
首先來看個例子,Song 是 Music 的子類:
class Music: Codable {
var kind: String?
}
class Song: Music {
var name: String?
}
JSON 數(shù)據(jù)為:
let jsonString = """
{
"kind": "popular",
"name": "Something Just Like This"
}
"""
數(shù)據(jù)解析:
if let jsonData = jsonString.data(using: String.Encoding.utf8) {
if let song = try? JSONDecoder().decode(Song.self, from: jsonData) {
dump(song)
}
}
結(jié)果:
? JSONDecoderDemo.Song #0
? super: JSONDecoderDemo.Music
? kind: Optional("popular")
- some: "popular"
- name: nil
通過上面的結(jié)果發(fā)現(xiàn),Song 類的實例只解析出了父類中的 kind 字段,而自己的 name 未能解析,這說明 Codable 在繼承中是無效的,當你在派生類中聲明遵循該協(xié)議時,會報如下錯誤:
Redundant conformance of 'Song' to protocol 'Decodable'
Redundant conformance of 'Song' to protocol 'Encodable'
那如何才能解決這個問題呢!
這時候,就需要我們自行實現(xiàn) Codable 協(xié)議了,代碼如下:
class Song: Music {
var name: String?
enum CodingKeys: String, CodingKey {
case type
case name
}
init(type: String, name songName:String) {
self.name = songName
super.init(type: type)
}
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
let container = try decoder.container(keyedBy: CodingKeys.self)
type = try container.decode(String.self, forKey: .type)
name = try container.decode(String.self, forKey: .name)
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(type, forKey: .type)
try container.encode(name, forKey: .name)
}
}
轉(zhuǎn)換結(jié)果:
? JSONDecoderDemo.Song #0
? super: JSONDecoderDemo.Music
? type: Optional("popular")
- some: "popular"
? name: Optional("Something Just Like This")
- some: "Something Just Like This"
通過上面的結(jié)果顯示,我已經(jīng)成功將 JSON 轉(zhuǎn)成了相應的數(shù)據(jù)模型,那么對派生類的處理,我們只需要參考上面的代碼,自行實現(xiàn) Codable 協(xié)議,就可以避免上述的錯誤。
數(shù)據(jù)模型轉(zhuǎn) JSON
當實現(xiàn) Codable 協(xié)議的某個對象想要轉(zhuǎn)為 JSON 時,則可以借助 JSONEncoder 編碼器來實現(xiàn)。
這個轉(zhuǎn)換相對來說就比較簡單了,這里就舉個簡單的例子吧!
let song = Song(type: "popular", name: "Something Just Like This")
if let jsonData = try? JSONEncoder().encode(song){
if let jsonString = String.init(data: jsonData, encoding: String.Encoding.utf8){
print("jsonString:" + "\(jsonString)")
}
}
輸出結(jié)果:
jsonString:{"type":"popular","name":"Something Just Like This"}
數(shù)據(jù)模型轉(zhuǎn) JSON 就完成了,So Easy。

結(jié)語
到這里本篇文章就結(jié)束了,首先非常感謝大家能耐著性子看到這里,說實話我在準備這篇文章的時候也有點痛苦,越寫越無聊,時常在寫的過程中腦子一直在想:這么無聊的內(nèi)容連我自己都寫不下去了,會有讀者愿意看嗎?但是開弓沒有回頭箭,畢竟我也花了幾天時間準備了素材,所以還是耐著寂寞寫完了,內(nèi)容過于枯燥,希望大家別嫌棄。

推薦閱讀:
優(yōu)雅的處理網(wǎng)絡數(shù)據(jù),你真的會嗎?不如看看這篇.
UICollectionView 自定義布局!看這篇就夠了!
點擊 關注我的公眾號
如果你想要跟大家分享你的文章,歡迎投稿
(*^▽^*)
