iOS 解碼/編碼

JSONEncoder / JSONDecoder

一個(gè)類型通過聲明自己遵守 Encodable 和/或 Decodable 協(xié)議匿辩,來表明可以被序列化和/或反序列化。這兩個(gè)協(xié)議都只約束了一個(gè)方法套硼,其中:Encodable 約束了 encode(to:),它定義了一個(gè)類型如何對自身進(jìn)行編碼;而 Decodable 則約束了一個(gè)初始化方法允华,用來從序列化的數(shù)據(jù)中創(chuàng)建實(shí)例:

/// 一個(gè)類型可以將自身編碼為某種外部表示形式井联。
public protocol Encodable {
/// 將值編碼到給定的 encoder 中卜壕。
public func encode(to encoder: Encoder) throws
}
/// 一個(gè)類型可以從某種外部表示形式中解碼得到自身。
public protocol Decodable {
/// 從給定的 decoder 中解碼來創(chuàng)建新的實(shí)例烙常。
public init(from decoder: Decoder) throws
}

Encoding / Decoding

struct SPUserModel: Codable {
    var name: String
    var contact: SPContactModel?
}
struct SPContactModel: Codable {
    var mobileTelephone = ""
    var fixedTelephone = ""
}

Encoding

let models = [SPUserModel(name: "zhangsan", contact: SPContactModel(mobileTelephone: "138xxxxxxxx", fixedTelephone: "010-xxxxxxx")),
              SPUserModel(name: "lisi", contact: SPContactModel(mobileTelephone: "135xxxxxxxx", fixedTelephone: "020-xxxxxxx"))]

do {
    let encoder = JSONEncoder()
    let jsonData = try encoder.encode(models)
    let jsonString = String(decoding: jsonData, as: UTF8.self)
    dump(jsonString)
} catch { }

Decoding

do {
    let decoder = JSONDecoder()
    let decoded = try decoder.decode([SPUserModel].self, from: jsonData)
    dump(decoded)
} catch { }

合成的代碼

Coding Keys

SPUserModel 里轴捎,編譯器會生成一個(gè)叫做 CodingKeys 的私有枚舉類型鹤盒。這個(gè)枚舉包含的成員與結(jié)構(gòu)體中的存儲屬性一一對應(yīng)。

private enum CodingKeys: String, CodingKey {
    case name
    case contact
}

encode(to:) 方法

下面是編譯器為 SPUserModel 結(jié)構(gòu)體生成的 encode(to:) 方法:

func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)
    try container.encode(contact, forKey: .contact)
}

init(from:) 初始化方法

當(dāng)我們調(diào)用 try decoder.decode([SPUserModel].self, from: jsonData) 時(shí)侦副,解碼器會按照我們傳入的類型 (這里是 [SPUserModel])侦锯,使用 Decodable 中定義的初始化方法創(chuàng)建一個(gè)該類型的實(shí)例。

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    contact = try container.decode(SPContactModel.self, forKey: .contact)
}

手動遵守協(xié)議

自定義 Coding Keys

我們可以創(chuàng)建自定義的 CodingKeys 枚舉秦驯,在這個(gè)枚舉中尺碰,我們可以:

  • 在編碼后的輸出中,用明確指定的字符串值重命名字段译隘。
  • 將某個(gè)鍵從枚舉中移除亲桥,以此跳過與之對應(yīng)字段。

想要設(shè)置一個(gè)不同的名字固耘,我們需要明確將枚舉的底層類型設(shè)置為 String题篷。例如, API 數(shù)據(jù)某個(gè)字段 name 更改為與模型不匹配的字段 username厅目,則需要自定義編碼鍵番枚,添加以下代碼,枚舉 CodingKeys 中包含 SPUserModel 模型中所有的屬性璧瞬,如此則可以正常解碼户辫。

如果枚舉里沒有包含 name 鍵,因此編碼時(shí) name 將會被跳過嗤锉,只有 contact 會被編碼渔欢,被跳過的屬性必須賦個(gè)默認(rèn)值,不然將會編譯失敗瘟忱。

let json = """
[{
    "username": "zhangsan",
    "contact": {"mobileTelephone": "138xxxxxxxx",
        "fixedTelephone": "010-xxxxxxx"
    }
},
{
    "username": "lisi",
    "contact": {"mobileTelephone": "138xxxxxxxx",
        "fixedTelephone": "010-xxxxxxx"
    }
}]
"""
struct SPUserModel: Codable {
    var name = ""
    var contact: SPContactModel?
    
    private enum CodingKeys: String, CodingKey {
        case name = "username"
        case contact
    }
}
struct SPContactModel: Codable {
    var mobileTelephone = ""
    var fixedTelephone = ""
}
do {
    let jsonData = json.data(using: .utf8)
    let decoder = JSONDecoder()
    let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
    dump(decoded)
} catch { }

自定義的 encode(to:) 和 init(from:) 實(shí)現(xiàn)

JSONEncoderJSONDecoder 默認(rèn)就可以處理可選值奥额。當(dāng)目標(biāo)類型中的一個(gè)屬性是可選值,如果數(shù)據(jù)中對應(yīng)的值不存在的話访诱,解碼器將會正確地跳過這個(gè)屬性垫挨。如下面的 contact 屬性

let json = """
[{
    "name": "zhangsan"
},
{
    "name": "lisi"
}]
"""
struct SPUserModel: Codable {
    var name = ""
    var contact: SPContactModel?
}
struct SPContactModel: Codable {
    var mobileTelephone = ""
    var fixedTelephone = ""
}

如果數(shù)據(jù)和所期待的形式不同,則解碼錯(cuò)誤触菜。 比如給 contact 對象一個(gè)空json對象

let json = """
[{
    "name": "zhangsan",
    "contact": { }
},
{
    "name": "lisi",
    "contact": { }
}]
"""

error: The data couldn’t be read because it is missing.

do {
    let jsonData = json.data(using: .utf8)
    let decoder = JSONDecoder()
    let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
} catch {
    //The data couldn’t be read because it is missing.
    print(error.localizedDescription)
}

重載 Decodable 的初始化方法 init(from:)九榔,明確地捕獲我們所期待的錯(cuò)誤,解碼器就可以成功地解碼這個(gè)錯(cuò)誤的 JSON 了

struct SPUserModel: Codable {
    var name = ""
    var contact: SPContactModel?
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        do {
            self.contact = try container.decodeIfPresent(SPContactModel.self, forKey: .contact)
        } catch DecodingError.keyNotFound {
            self.contact = nil
        }
    }
}
struct SPContactModel: Codable {
    var mobileTelephone = ""
    var fixedTelephone = ""
}

常見的編碼任務(wù)

讓其他人的代碼滿足 Codable

假如 SPUserModel 中存在并不滿足 Codable 協(xié)議的類涡相,比如 CLLocationCoordinate2D 哲泊,編譯器現(xiàn)在會 (正確地) 抱怨說它無法為 SPUserModel 自動生成實(shí)現(xiàn) Codable 的代碼,因?yàn)樗? coordinate 屬性不再是遵從 Codable 的類型了催蝗。

struct SPUserModel: Codable {
    var name = ""
    var coordinate: CLLocationCoordinate2D
}

解決辦法1

struct SPUserModel: Codable {
    var name = ""
    var coordinate: CLLocationCoordinate2D
    
    private enum CodingKeys: String, CodingKey {
        case name
        case latitude = "lat"
        case longitude = "lon"
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        // 分別編碼緯度和經(jīng)度
        try container.encode(coordinate.latitude, forKey: .latitude)
        try container.encode(coordinate.longitude, forKey: .longitude)
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        // 從緯度和經(jīng)度重新構(gòu)建 CLLocationCoordinate2D
        self.coordinate = CLLocationCoordinate2D (
            latitude: try container.decode(Double.self, forKey: .latitude),
            longitude: try container.decode(Double.self, forKey: .longitude)
        )
    }
}
let json = """
[{
    "name": "zhangsan",
    "lat": 312312313,
    "lon": 3452423424
},
{
    "name": "lisi",
    "lat": 123132343,
    "lon": 3453432423
}]
"""

do {
    let jsonData = json.data(using: .utf8)
    let decoder = JSONDecoder()
    let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
    //Optional(__C.CLLocationCoordinate2D(latitude: 312312313.0, longitude: 3452423424.0))
    dump(decoded)
} catch { }

解決辦法2:嵌套容器

struct SPUserModel: Codable {
    var name = ""
    var coordinate: CLLocationCoordinate2D
    
    private enum CodingKeys: String, CodingKey {
        case name
        case coordinate
    }
    
    // 嵌套容器的編碼鍵
    private enum CoordinateCodingKeys: CodingKey {
        case latitude
        case longitude
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        var coordinateContainer = container.nestedContainer(keyedBy: CoordinateCodingKeys.self, forKey: .coordinate)
        try coordinateContainer.encode(coordinate.latitude, forKey: .latitude)
        try coordinateContainer.encode(coordinate.longitude, forKey: .longitude)
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        let coordinateContainer = try container.nestedContainer(keyedBy: CoordinateCodingKeys.self, forKey: .coordinate)
        self.coordinate = CLLocationCoordinate2D (
            latitude: try coordinateContainer.decode(Double.self, forKey: .latitude),
            longitude: try coordinateContainer.decode(Double.self, forKey: .longitude)
        )
    }
}
let json = """
[{
    "name": "zhangsan",
    "coordinate": {
        "latitude": 279886268,
        "longitude": 123678613
                  }
},
{
    "name": "lisi",
    "coordinate": {
        "latitude": 221311,
        "longitude": 67868
                  }
}]
"""

do {
    let jsonData = json.data(using: .utf8)
    let decoder = JSONDecoder()
    let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
    //Optional(__C.CLLocationCoordinate2D(latitude: 279886268.0, longitude: 123678613.0))
    dump(decoded)
} catch { }

解決辦法3

struct SPCoordinate: Codable {
    var latitude: Double
    var longitude: Double
}

struct SPUserModel: Codable {
    
    var name: String
    private var _coordinate: SPCoordinate
    var coordinate: CLLocationCoordinate2D {
        get {
            return CLLocationCoordinate2D(latitude: _coordinate.latitude,
                                          longitude: _coordinate.longitude)
        }
        set {
            _coordinate = SPCoordinate(latitude: newValue.latitude,
                                     longitude: newValue.longitude)
        }
    }
    private enum CodingKeys: String, CodingKey {
        case name
        case _coordinate = "coordinate"
    }
}
let json = """
[{
    "name": "zhangsan",
    "coordinate": {
        "latitude": 279886268,
        "longitude": 123678613
                  }
},
{
    "name": "lisi",
    "coordinate": {
        "latitude": 221311,
        "longitude": 67868
                  }
}]
"""

do {
    let jsonData = json.data(using: .utf8)
    let decoder = JSONDecoder()
    let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
    //Optional(__C.CLLocationCoordinate2D(latitude: 279886268.0, longitude: 123678613.0))
    dump(decoded)
} catch { }

tips

1. 屬性樣式轉(zhuǎn)換(mobileTelephone -> mobile_telephone)

假如 API 數(shù)據(jù)某個(gè)字段 mobileTelephone 更改為 mobile_telephone 樣式切威,則會出現(xiàn)錯(cuò)誤 :

let json = """
[{
    "name": "zhangsan",
    "contact": {"mobile_telephone": "138xxxxxxxx",
        "fixed_telephone": "010-xxxxxxx"
    }
},
{
    "name": "lisi",
    "contact": {"mobile_telephone": "138xxxxxxxx",
        "fixed_telephone": "010-xxxxxxx"
    }
}]
"""

do {
    let jsonData = json.data(using: .utf8)
    let decoder = JSONDecoder()
    let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
    print(decoded.first?.contact?.fixedTelephone)
} catch {
    //error: The data couldn’t be read because it is missing.
    print(error.localizedDescription)
}
struct SPUserModel: Codable {
    var name: String
    var contact: SPContactModel?
}
struct SPContactModel: Codable {
    var mobileTelephone = ""
    var fixedTelephone = ""
}

在需要編碼和解碼的地方添加以下代碼

encoder.keyEncodingStrategy = .convertToSnakeCase
decoder.keyDecodingStrategy = .convertFromSnakeCase

2.嵌套類型改變

假如 API 數(shù)據(jù)嵌套類型更改,而我們?nèi)匀幌M褂们短住?/p>

let json = """
[{
    "name": "zhangsan",
    "mobile_telephone": "138xxxxxxxx",
    "fixed_telephone": "010-xxxxxxx"
},
{
    "name": "lisi",
    "mobile_telephone": "138xxxxxxxx",
    "fixed_telephone": "010-xxxxxxx"
}]
"""

do {
    let jsonData = json.data(using: .utf8)
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
} catch { }
struct SPUserModel: Codable {
    var name: String
    var contact: SPContactModel?
    
    private enum CodingKeys: CodingKey {
        case name
        case mobileTelephone
        case fixedTelephone
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(contact?.mobileTelephone, forKey: .mobileTelephone)
        try container.encode(contact?.fixedTelephone, forKey: .fixedTelephone)
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        self.contact = SPContactModel (
            mobileTelephone: try container.decode(String.self, forKey: .mobileTelephone),
            fixedTelephone: try container.decode(String.self, forKey: .fixedTelephone)
        )
    }
}
struct SPContactModel: Codable {
    var mobileTelephone = ""
    var fixedTelephone = ""
}

3.日期的編解碼

encoder.dateEncodingStrategy = .formatted(<#T##DateFormatter#>)
decoder.dateDecodingStrategy = .formatted(<#T##DateFormatter#>)

假如我們希望日期的格式為 "yyyy-MM-dd"

extension DateFormatter {
  static let dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd"
    return formatter
  }()
}
struct SPUserModel: Codable {
    var name: String
    var birthday: Date?
}

需要設(shè)置解編碼的 dateEncodingStrategy 屬性

let models = [SPUserModel(name: "zhangsan", birthday: Date()),
              SPUserModel(name: "lisi", birthday: Date())]
        
do {
    let encoder = JSONEncoder()
    encoder.dateEncodingStrategy = .formatted(.dateFormatter)
    let jsonData = try encoder.encode(models)
    let jsonString = String(decoding: jsonData, as: UTF8.self)
} catch { }

do {
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .formatted(.dateFormatter)
    let decoded = try decoder.decode([SPUserModel].self, from: self.jsonData)
} catch { }

PropertyListEncoder / PropertyListDecoder

class SPUserModel: NSObject, Codable {
    var name: String
    var address: String
    
    init(name: String, address: String) {
        self.name = name
        self.address = address
    }
}

Encoding

let models = [SPUserModel(name: "zhangsan", address: "beijing"),
              SPUserModel(name: "lisi", address: "shanghai")]

do {
    let data = try PropertyListEncoder().encode(models)
    let data2 = try NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: true)
    UserDefaults.standard.set(data2, forKey: "key")
} catch { }

Decoding

guard let data = UserDefaults.standard.object(forKey: "key") else { return }
do {
    let data2 = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [SPUserModel.self], from: data as! Data)
    let model = try PropertyListDecoder().decode([SPUserModel].self, from: data2 as! Data)
    dump(model)
} catch  { }

--- ---

Encoding

let models = [SPUserModel(name: "zhangsan5", address: "beijing"),
              SPUserModel(name: "lisi3", address: "shanghai")]

do {
    let data = try PropertyListEncoder().encode(models)
    NSKeyedArchiver.archiveRootObject(data, toFile: NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first! + "/info")
} catch { }

Decoding

guard let data = NSKeyedUnarchiver.unarchiveObject(withFile: NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first! + "/info") else { return }
do {
    let model = try PropertyListDecoder().decode([SPUserModel].self, from: data as! Data)
} catch  { }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末丙号,一起剝皮案震驚了整個(gè)濱河市先朦,隨后出現(xiàn)的幾起案子缰冤,更是在濱河造成了極大的恐慌,老刑警劉巖喳魏,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棉浸,死亡現(xiàn)場離奇詭異,居然都是意外死亡截酷,警方通過查閱死者的電腦和手機(jī)涮拗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進(jìn)店門乾戏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來迂苛,“玉大人,你說我怎么就攤上這事鼓择∪茫” “怎么了?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵呐能,是天一觀的道長念搬。 經(jīng)常有香客問我,道長摆出,這世上最難降的妖魔是什么朗徊? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮偎漫,結(jié)果婚禮上爷恳,老公的妹妹穿的比我還像新娘。我一直安慰自己象踊,他們只是感情好温亲,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著杯矩,像睡著了一般栈虚。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上史隆,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天魂务,我揣著相機(jī)與錄音,去河邊找鬼泌射。 笑死粘姜,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的魄幕。 我是一名探鬼主播相艇,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼纯陨!你這毒婦竟也來了坛芽?” 一聲冷哼從身側(cè)響起留储,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎咙轩,沒想到半個(gè)月后获讳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡活喊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年丐膝,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钾菊。...
    茶點(diǎn)故事閱讀 40,115評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡帅矗,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出煞烫,到底是詐尸還是另有隱情浑此,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布滞详,位于F島的核電站凛俱,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏料饥。R本人自食惡果不足惜蒲犬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望岸啡。 院中可真熱鬧原叮,春花似錦、人聲如沸凰狞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赡若。三九已至达布,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間逾冬,已是汗流浹背黍聂。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留身腻,地道東北人产还。 一個(gè)月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像嘀趟,于是被迫代替她去往敵國和親脐区。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評論 2 355