iOS 解码/编码

JSONEncoder / JSONDecoder

一个类型通过声明自己遵守 Encodable 和/或 Decodable 协议,来表明可以被序列化和/或反序列化。这两个协议都只约束了一个方法,其中:Encodable 约束了 encode(to:),它定义了一个类型如何对自身进行编码;而 Decodable 则约束了一个初始化方法,用来从序列化的数据中创建实例:

/// 一个类型可以将自身编码为某种外部表示形式。
public protocol Encodable {
/// 将值编码到给定的 encoder 中。
public func encode(to encoder: Encoder) throws
}
/// 一个类型可以从某种外部表示形式中解码得到自身。
public protocol Decodable {
/// 从给定的 decoder 中解码来创建新的实例。
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 里,编译器会生成一个叫做 CodingKeys 的私有枚举类型。这个枚举包含的成员与结构体中的存储属性一一对应。

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

encode(to:) 方法

下面是编译器为 SPUserModel 结构体生成的 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:) 初始化方法

当我们调用 try decoder.decode([SPUserModel].self, from: jsonData) 时,解码器会按照我们传入的类型 (这里是 [SPUserModel]),使用 Decodable 中定义的初始化方法创建一个该类型的实例。

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

手动遵守协议

自定义 Coding Keys

我们可以创建自定义的 CodingKeys 枚举,在这个枚举中,我们可以:

  • 在编码后的输出中,用明确指定的字符串值重命名字段。
  • 将某个键从枚举中移除,以此跳过与之对应字段。

想要设置一个不同的名字,我们需要明确将枚举的底层类型设置为 String。例如, API 数据某个字段 name 更改为与模型不匹配的字段 username,则需要自定义编码键,添加以下代码,枚举 CodingKeys 中包含 SPUserModel 模型中所有的属性,如此则可以正常解码。

如果枚举里没有包含 name 键,因此编码时 name 将会被跳过,只有 contact 会被编码,被跳过的属性必须赋个默认值,不然将会编译失败。

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:) 实现

JSONEncoderJSONDecoder 默认就可以处理可选值。当目标类型中的一个属性是可选值,如果数据中对应的值不存在的话,解码器将会正确地跳过这个属性。如下面的 contact 属性

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

如果数据和所期待的形式不同,则解码错误。 比如给 contact 对象一个空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:),明确地捕获我们所期待的错误,解码器就可以成功地解码这个错误的 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 = ""
}

常见的编码任务

让其他人的代码满足 Codable

假如 SPUserModel 中存在并不满足 Codable 协议的类,比如 CLLocationCoordinate2D ,编译器现在会 (正确地) 抱怨说它无法为 SPUserModel 自动生成实现 Codable 的代码,因为它的 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)
        // 分别编码纬度和经度
        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)
        // 从纬度和经度重新构建 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. 属性样式转换(mobileTelephone -> mobile_telephone)

假如 API 数据某个字段 mobileTelephone 更改为 mobile_telephone 样式,则会出现错误 :

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 数据嵌套类型更改,而我们仍然希望使用嵌套。

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

需要设置解编码的 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  { }

你可能感兴趣的:(iOS 解码/编码)