iOS开发 - Swift Codable协议实战:快速、简单、高效地完成JSON和Model转换!

前言

Codable 是 Swift 4.0 引入的一种协议,它是一个组合协议,由 Decodable 和 Encodable 两个协议组成。它的作用是将模型对象转换为 JSON 或者是其它的数据格式,也可以反过来将 JSON 数据转换为模型对象。

Encodable 和 Decodable 分别定义了 encode(to:) 和 init(from:) 两个协议函数,分别用来实现数据模型的归档和外部数据的解析和实例化。最常用的场景就是刚提到的 JSON 数据与模型的相互转换,但是 Codable 的能力并不止于此。

简单应用

在实际开发中,Codable 的使用非常方便,只需要让模型遵循 Codable 协议即可:

复制代码
struct GCPerson: Codable {
    var name: String
    var age: Int
    var height: Float // cm
    var isGoodGrades: Bool
}

接下来编写数据编码和解码的方法:

复制代码
func encodePerson() {
	let person = GCPerson(name: "XiaoMing", age: 16, height: 160.5, isGoodGrades: true)
	let encoder = JSONEncoder()
	encoder.outputFormatting = .prettyPrinted // 优雅永不过时,json会好看点哟
	do {
		let data = try encoder.encode(person)
		let jsonStr = String(data: data, encoding: .utf8)
		textView.text = jsonStr
		print(jsonStr as Any)
	} catch let err {
		print("err", err)
	}
}

func decodePerson() {
	let jsonStr = "{\"age\":16,\"isGoodGrades\":1,\"name\":\"XiaoMing\",\"height\":160.5}"
	guard let data = jsonStr.data(using: .utf8) else {
		print("get data fail")
		return
	}
	let decoder = JSONDecoder()
	do {
		let person = try decoder.decode(GCPerson.self, from: data)
		print(person)
	} catch let err {
		print("err", err)
	}
}

上面例子的输出:

复制代码
Optional("{\n  \"age\" : 16,\n  \"isGoodGrades\" : true,\n  \"name\" : \"XiaoMing\",\n  \"height\" : 160.5\n}")
GCPerson(name: "XiaoMing", age: 16, height: 160.5, isGoodGrades: false)

应该有眼尖的童鞋是发现了,我将 JSONEncoder 的 outputFormatting 设置为了 prettyPrinted,这会让它输出的时候会美观一下,比如将它们放置在 UITextView 视图中作对比:

这里指的 default 是在没有设置 outputFormatting 的默认情况

CodingKeys 字段映射

如果属性名称与 JSON 数据中的键名不一致,需要使用 Swift 语言中的 CodingKeys 枚举来映射属性名称和键名。CodingKeys 是一个遵循了 CodingKey 协议的枚举,它可以用来描述 Swift 对象的属性与 JSON 数据中的键名之间的映射关系。

复制代码
struct Address: Codable {
    var zipCode: Int
    var fullAddress: String
    
    enum CodingKeys: String, CodingKey {
        case zipCode = "zip_code"
        case fullAddress = "full_address"
    }
}

数据编码和解码的方法与前面的大同小异:

复制代码
func encodeAddress() {
	let address = Address(zipCode: 528000, fullAddress: "don't tell you")
	let encoder = JSONEncoder()
	encoder.outputFormatting = .prettyPrinted // 优雅永不过时,json会好看点哟
	do {
		let data = try encoder.encode(address)
		let jsonStr = String(data: data, encoding: .utf8)
		textView.text.append("\n\n")
		textView.text = textView.text.appending(jsonStr ?? "")
		print(jsonStr as Any)
	} catch let err {
		print("err", err)
	}
}

func decodeAddress() {
	let jsonStr = "{\"zip_code\":528000,\"full_address\":\"don't tell you\"}"
	guard let data = jsonStr.data(using: .utf8) else {
		print("get data fail")
		return
	}
	let decoder = JSONDecoder()
	do {
		let address = try decoder.decode(Address.self, from: data)
		print(address)
	} catch let err {
		print("err", err)
	}
}

此时的输出为:

复制代码
Optional("{\n  \"zip_code\" : 528000,\n  \"full_address\" : \"don\'t tell you\"\n}")
Address(zipCode: 528000, fullAddress: "don\'t tell you")

从控制台日志可以看出,Address 模型中的的 zipCode 和 fullAddress 属性字段已被替换为 zip_code 和 full_address,值得注意的是,使用 CodingKeys 映射后就只能使用映射后的字段名称。

数据类型匹配

Swift 中的数据类型需要与 JSON 数据中的数据类型匹配,否则将无法正确地进行解码。如果数据类型不匹配,则会进入到 catch 代码块,意味着解码失败。

复制代码
let jsonStr = "{\"age\":16,\"isGoodGrades\":1,\"name\":\"XiaoMing\",\"height\":160.5}"

在上面的例子中,将 isGoodGrades 的值改为1,此时输出的错误内容为:

复制代码
err typeMismatch(Swift.Bool, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "isGoodGrades", intValue: nil)], debugDescription: "Expected to decode Bool but found a number instead.", underlyingError: nil))

由此引出,Bool 型只支持 true 和 false,其它一概不认。

注意:只要是其中一个数据字段不能解析,则整条解析失败。

Date 和 Optional 可选类型

在使用 Codable 对 Date 和 Optional 属性进行编解码时,有些细节是需要了解的。

Codable 默认启用的时间策略是 deferredToDate,即从 UTC时间2001年1月1日0时0分0秒 开始的秒数,对应 Date 类型中 timeIntervalSinceReferenceDate 这个属性。比如 702804983.44863105 这个数字解析后的结果是 2023-04-10 07:34:17 +0000。

在这儿把时间策略设置为 secondsSince1970,因为这个会比上面的要常用。我们需将 JSONEncoder 的 dateEncodingStrategy 设置为 secondsSince1970,JSONDecoder 也是相同的设置。

在设置 Optional 可选类型时,在编码时,为空的属性不会包含在 JSON 数据中。在解码时,直接不传或将值设定为 \"null\" / \"nil\" / null 这三种值也能被解析为 nil。

复制代码
struct Activity: Codable {
    var time: Date
    var url: URL?
}

编码解码的工作:

复制代码
func encodeActivity() {
	let activity = Activity(time: Date(), url: URL(string: "https://www.baidu.com"))
	let encoder = JSONEncoder()
	encoder.outputFormatting = .prettyPrinted // 优雅永不过时,json会好看点哟
	encoder.dateEncodingStrategy = .secondsSince1970 // 秒
	do {
		let data = try encoder.encode(activity)
		let jsonStr = String(data: data, encoding: .utf8)
		textView.text.append("\n\n")
		textView.text = textView.text.appending(jsonStr ?? "")
		print(jsonStr as Any)
	} catch let err {
		print("err", err)
	}
}

func decodeActivity() {
//        let jsonStr = "{\"time\":528000,\"url\":111}" // 即便是 Optional 的属性也要对应的数据类型,否则还是会解析失败
	let jsonStr = "{\"time\":1681055185}" // Optional类型的属性字段,直接不传也是nil
	//        let jsonStr = "{\"time\":528000,\"url\":null}" // 以下三种也能被解析为nil,\"null\" / \"nil\" / null
	guard let data = jsonStr.data(using: .utf8) else {
		print("get data fail")
		return
	}
	let decoder = JSONDecoder()
	decoder.dateDecodingStrategy = .secondsSince1970 // 秒
	do {
		let activity = try decoder.decode(Activity.self, from: data)
		print(activity)
	} catch let err {
		print("err", err)
	}
}

此时的输出为:

复制代码
Optional("{\n  \"url\" : \"https:\\/\\/www.baidu.com\",\n  \"time\" : 1681057020.835813\n}")
Activity(time: 2023-04-09 15:46:25 +0000, url: nil)

自定义编解码

有时候前后端定义的模型不同时,有可能会需要用到自定义编解码,以此来达成"统一"。

比如我们现在有一个 Dog 模型,sex 字段为 Bool 型,在后端的定义为 0 和 1,此时我们需要将它们给转换起来,可以是 false 为 0,true 为 1。

复制代码
struct Dog: Codable {
    var name: String
    var sex: Bool // 0/false女 1/true男
    
    init(name: String, sex: Bool) {
        self.name = name
        self.sex = sex
    }
    
    // 必须实现此枚举,在编码解码方法中需要用到
    enum CodingKeys: CodingKey {
        case name
        case sex
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        // 取出来int后再转换为Bool
        let sexInt = try container.decode(Int.self, forKey: .sex)
        sex = sexInt == 1
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.name, forKey: .name)
        // 将sex属性以int类型编码
        try container.encode(sex ? 1 : 0, forKey: .sex)
    }
}

在编码的时候将 sex 从 Bool 型转换为 Int 型,解码时则反过来。编解码的工作依旧与前面的大致一样:

复制代码
func encodeDog() {
	let dog = Dog(name: "Max", sex: true)
	let encoder = JSONEncoder()
	encoder.outputFormatting = .prettyPrinted // 优雅永不过时,json会好看点哟
	do {
		let data = try encoder.encode(dog)
		let jsonStr = String(data: data, encoding: .utf8)
		textView.text.append("\n\n")
		textView.text = textView.text.appending(jsonStr ?? "")
		print(jsonStr as Any)
	} catch let err {
		print("err", err)
	}
}

func decodeDog() {
	let jsonStr = "{\"name\":\"Max\",\"sex\":1}"
	guard let data = jsonStr.data(using: .utf8) else {
		print("get data fail")
		return
	}
	let decoder = JSONDecoder()
	do {
		let dog = try decoder.decode(Dog.self, from: data)
		print(dog)
	} catch let err {
		print("err", err)
	}
}

此时的日志输出为:

复制代码
Optional("{\n  \"name\" : \"Max\",\n  \"sex\" : 1\n}")
Dog(name: "Max", sex: true)

总结

Codable 是 Swift 中非常方便的一个协议,可以帮助我们快速进行数据的编码和解码,提高了开发效率和代码可读性。当然使用不当也会造成严重的灾难,所以我为大家整理了以下几点使用时的注意事项,希望能对大家有所帮助:

  1. 嵌套的数据结构也需要遵循 Codable 协议。
  2. Bool 型只支持 true 或 false。
  3. Optional 类型修饰的属性字段,直接不传是 nil,或将值设定为以下三种也能被解析为 nil,\"null\" / \"nil\" / null。
  4. 可以使用自定义的编码器和解码器来进行转换。
相关推荐
茶底世界之下6 小时前
类型擦除之后,Metal 滤镜组合为什么不能只执行一个 Filter?
ios·swift
传奇开心果编程8 小时前
【SwiftUI提高练中学】第8课 网络层架构:拦截器、重试、缓存与离线优先
学习·macos·ui·ios·swiftui
liangshanbo121514 小时前
高级前端面试题:package.json 常见字段
前端·json
传奇开心果编程15 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
tink16 小时前
告别 Xcode IDE:用 VS Code + SweetPad + XcodeGen 开发 iOS 应用的完整指南
ios·swiftui·xcode
传奇开心果编程18 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程19 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程19 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
黑科技iOS上架19 小时前
iOS隐私合规扫描工具
ios·审核
梦帮科技19 小时前
【3.0修订版】创世与节点工程:genesis.json 与 node.toml 解剖
数据结构·算法·json·去中心化·区块链·智能合约·信任链