自定义编码和解码逻辑(SwiftData中实现Codable协议)
自定义编码和解码逻辑(SwiftData中实现Codable协议)

自定义编码和解码逻辑(SwiftData中实现Codable协议)

实际场景

下面是一个遵循Codable以及Hashable的类。

class Friend: Codable, Hashable {
    var id: String
    var name: String
    
    init(id: String, name: String) {
        self.id = id
        self.name = name
    }
    // 实现 Equatable 协议
    static func == (lhs: Friend, rhs: Friend) -> Bool {
        return lhs.id == rhs.id
    }
    
    // 实现 Hashable 协议
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

为实现SwiftData功能,在代码中引入SwiftData以及@Model,引入后发现存在以下报错:

Type 'Friend' does not conform to protocol 'Decodable'
Type 'Friend' does not conform to protocol 'Encodable'

这个错误是因为 @Model 属性包装器与 Codable 协议的实现有冲突。

@Model 是 SwiftData 的特性,它给类自动生成某些功能,如持久化存储。但 Codable 依赖于编译器生成的代码来支持编码和解码。由于 @Model 自动管理了一些属性,编译器无法正确地生成 Codable 的实现,因此报错。

这一问题实际上是可编解码的代码实现SwiftData功能时,必经的错误问题。

解决方案

通过给可编解码的类手动实现Encodable和Decodable,以绕过编译器的问题。

定义CodingKeys

Swift 的默认行为是使用属性名称直接作为键名。这种情况下,Codable 的自动合成功能会尝试使用类或结构体的属性名称与 JSON 或其他格式的数据中的键名进行一一对应。

但在某些场景下,这种默认行为可能无法满足需求,因此需要手动定义 CodingKeys。

enum CodingKeys: String, CodingKey {
    case id = "id"
    case name = "name"
}

这部分代码定义了一个枚举,用来表示对象的属性名称在编码和解码过程中与外部数据(如 JSON)的键值映射关系。

Swift 的 Codable 系统在编码和解码时会参考这个枚举,将属性名称和外部数据的键进行映射。

如果属性名称和键名相同,可以省略 = “键名”。

{
    "id": "12345",
    "name": "John Doe"
}

id 会映射到对象的 id 属性。

name 会映射到对象的 name 属性。

实现解码器的初始化方法

下面的代码实现了解码器的初始化方法,用于从外部数据(例如 JSON)中读取值并初始化对象。

required init(from decoder: any Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.id = try container.decode(String.self, forKey: .id)
    self.name = try container.decode(String.self, forKey: .name)
}
1、decoder构造方法
required init(from decoder: any Decoder) throws
required关键字

在类中实现一个构造方法并标记为 required,它会确保所有子类都支持该构造方法。

这是因为 Decodable 协议要求所有类型都必须有 init(from:) 方法来完成解码。

如果 Friend 类有子类,例如 BestFriend,那么子类也必须实现这个方法:

class BestFriend: Friend {
    required init(from decoder: any Decoder) throws {
        try super.init(from: decoder)  // 调用父类的解码逻辑
        // 子类的解码逻辑(如果需要)
    }
}
init(from decoder: any Decoder) throws

Swift 的 Decodable 协议明确要求实现一个构造方法,其方法签名是:

init(from decoder: Decoder) throws

1、方法名和参数名

方法名必须是 init。

第一个参数名必须是 from,这是协议明确规定的,不能随意更改。

2、参数类型

参数的类型必须是 Decoder。

3、抛出错误

方法必须支持抛出错误,添加 throws。

Swift 的协议(如 Decodable)定义了类型必须满足的要求

如果签名不匹配,就无法满足协议的要求。

编译器会检测并报错。

2、创建一个键值对容器
let container = try decoder.container(keyedBy: CodingKeys.self)

这段代码的核心作用是从解码器(Decoder)中创建一个键值对容器(KeyedDecodingContainer),然后通过这个容器来访问编码数据中的具体值

decoder对象

decoder 是实现了 Decoder 协议的对象。

这个对象负责解码输入数据(如 JSON、Plist 等)并将其转换为 Swift 类型的值。

例如,当解码 JSON 时,decoder 会把 JSON 解析为不同的结构(键值对、数组等),可以通过调用其方法获取数据。

使用 decoder.container 获取一个容器(KeyedDecodingContainer),用于访问 JSON 中的键值对。

这里的键是通过 CodingKeys 定义的。

3、解码赋值
self.id = try container.decode(String.self, forKey: .id)
self.name = try container.decode(String.self, forKey: .name)

将 JSON 中键为 id 和 name 的值分别解码为 String 类型,并赋值给 self.id 和 self.name。

作用

通过实现 init(from:),该类可以从 JSON 等外部格式的数据中构造一个对象实例。

示例

假设有以下 JSON 数据:

{
    "id": "001",
    "name": "Alice"
}

调用解码器:

let jsonData = """
{
    "id": "001",
    "name": "Alice"
}
""".data(using: .utf8)!

let decoder = JSONDecoder()
let friend = try decoder.decode(Friend.self, from: jsonData)
print(friend.id)   // 输出: 001
print(friend.name) // 输出: Alice

实现编码器的初始化方法

下面的代码实现了编码器的初始化方法,将对象的属性编码到外部格式(例如JSON)的方法。

func encode(to encoder: any Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(self.id, forKey: .id)
    try container.encode(self.name, forKey: .name)
}
1、encode方法
func encode(to encoder: any Encoder) throws { }

Encoder 是 Swift 的协议,提供了将对象编码为外部格式的功能。

跟Decoder一样,都是Swift 的 Encodable 协议明确要求实现一个构造方法:

func encode(to encoder: any Encoder) throws

这也意味着必须遵守Encodable协议要求实现的方法,且不能修改它。

2、创建一个键值对容器
var container = encoder.container(keyedBy: CodingKeys.self)

使用 encoder.container 获取一个容器(KeyedEncodingContainer),用于将数据编码为键值对。

键由 CodingKeys 定义。

3、属性值编码到容器中
try container.encode(self.id, forKey: .id)
try container.encode(self.name, forKey: .name)

.id、.name 是 CodingKeys 枚举的成员,对应模型的 id和name属性。

容器根据 CodingKeys.id 找到需要序列化的字段,并将 self.id 的值添加到序列化输出中。

通过编码逻辑,将对象的属性转换为 JSON 等外部格式。

JSONDecoder解码过程

下面是通过网络调取JSON数据的示例:

let (data,_) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" // 根据数据格式
decoder.dateDecodingStrategy = .formatted(dateFormatter)
let decodedResponse = try decoder.decode([Friend].self, from: data)
    print("进入解码内容")
    print("解码内容为:\(decodedResponse)")
return decodedResponse

获取到JSON数据后,在JSONDecoder()的decode方法中,会将获取到的Data类型数据,转换为对应的类型格式。

class Friend: Codable {
    var id: String = "0"
    var name: String = "fangjunyu"

    init(id: String = "0", name: String = "fangjunyu") {
        self.id = id
        self.name = name
    }
    
    enum CodingKeys: String, CodingKey {
        case id,name
    }

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

上面是一个简单的Swift对象,定义了CodingKeys和解码器的初始化方法。

JSONDecoder 的解码过程不只是创建解码器实例,而是包括多步操作,最终完成从原始 JSON 数据到 Swift 对象的转换。以下是详细的步骤:

1、创建 JSONDecoder 实例

let decoder = JSONDecoder()

这是初始化解码器的第一步,解码器准备好解析 JSON 数据。

可通过配置解码器的属性(如 dateDecodingStrategy 或 keyDecodingStrategy)来调整解码行为。

2、指定数据解码的目标类型

let decodedResponse = try decoder.decode([Friend].self, from: data)

decode(_:from:) 是解码的核心方法。

参数说明:

第一个参数是目标类型(如 [Friend].self,表示一个 Friend 对象的数组)。

第二个参数是要解码的二进制数据(data,通常是从网络请求或文件中获取到的 JSON 数据)。

3、解码过程的核心步骤

根据目标类型进行对象初始化

decode(_:from:) 会遍历 JSON 数据,并尝试将其映射到目标类型的实例。

在解析数组时:

每个 JSON 对象(如 {“id”: “1”, “name”: “Alice”})会被解码为目标类型的一个实例。

调用目标类型的 init(from:) 方法完成映射。

Decoder 创建容器

在调用模型的 init(from:) 方法时:

解码器会提供一个容器(如 KeyedDecodingContainer),用于访问当前 JSON 对象中的键值对。

例如,对于 Friend 的解码:

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

decoder.container(keyedBy:) 提供了一个容器,用来访问 JSON 数据中的键值对。

容器的类型是 KeyedDecodingContainer<CodingKeys>。

它的设计方式使可以通过键(CodingKeys)访问 JSON 数据中的字段值。

创建的 KeyedDecodingContainer容器,用来访问 JSON 数据中与当前上下文相关的键值对。

容器负责将 CodingKeys 转换为对应的 JSON 键,并在内部结构中查找字段值。

container.decode(_:forKey:) 通过键(如 id 和 name)提取并解析数据。

当调用 container.decode(String.self, forKey: .id) 时,容器会:

1、使用 CodingKeys.id 查找 JSON 数据中的 “id”。

2、提取值 “1”,并解析为目标类型 String。

类型验证与映射

解码器会验证数据类型是否匹配目标模型中的属性类型。

例如,id 和 name 的值必须是字符串。如果类型不匹配,会抛出错误。

匹配成功后,解码器会将值赋给模型对象的对应属性。

4、返回解码后的对象

当解码成功时,解码器会返回目标类型的实例(或实例数组)。

在示例中,decodedResponse 是 [Friend] 的实例数组。

JSONEncoder编码过程

因为JSONEncoder编码和JSONDecoder解码过程类似,最后将数据通过容器编码到JSON输出中,此处略过。

完整代码

import Foundation
import SwiftData

@Model
class Friend: Codable, Hashable {
    var id: String = "0"
    var name: String = "fangjunyu"
    
    enum CodingKeys: String, CodingKey {
        case id,name
    }
    
    init(id: String, name: String) {
        self.id = id
        self.name = name
    }
    
    required init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(String.self, forKey: .id)
        self.name = try container.decode(String.self, forKey: .name)
    }
    
    func encode(to encoder: any Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.id, forKey: .id)
        try container.encode(self.name, forKey: .name)
    }
    
    // 实现 Equatable 协议
    static func == (lhs: Friend, rhs: Friend) -> Bool {
        return lhs.id == rhs.id
    }
    
    // 实现 Hashable 协议
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
    
}
@Model
class Friendface:Codable, Hashable {
    var id: String = "0"
    var isActive: Bool = true
    var name: String = "fangjunyu"
    var age: Int = 27
    var email: String = "fangjunyu.com@gmail.com"
    var address: String = "山东省日照市"
    var about: String = "关于SwiftData"
    var registered: Date = Date.now
    var tags: [String] = ["people"]
    var friends: [Friend] = [Friend(id: "0", name: "fangjunyu")]

    enum CodingKeys: String, CodingKey {
        case id,isActive,name,age,email,address,about,registered,tags,friends
    }
    
    init(id: String, isActive: Bool, name: String, age: Int, email: String, address: String, about: String, registered: Date, tags: [String], friends: [Friend]) {
        self.id = id
        self.isActive = isActive
        self.name = name
        self.age = age
        self.email = email
        self.address = address
        self.about = about
        self.registered = registered
        self.tags = tags
        self.friends = friends
    }
    
    required init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(String.self, forKey: .id)
        self.isActive = try container.decode(Bool.self, forKey: .isActive)
        self.name = try container.decode(String.self, forKey: .name)
        self.age = try container.decode(Int.self, forKey: .age)
        self.email = try container.decode(String.self, forKey: .email)
        self.address = try container.decode(String.self, forKey: .address)
        self.about = try container.decode(String.self, forKey: .about)
        self.registered = try container.decode(Date.self, forKey: .registered)
        self.tags = try container.decode([String].self, forKey: .tags)
        self.friends = try container.decode([Friend].self, forKey: .friends)
    }
    
    func encode(to encoder: any Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.id, forKey: .id)
        try container.encode(self.isActive, forKey: .isActive)
        try container.encode(self.name, forKey: .name)
        try container.encode(self.age, forKey: .age)
        try container.encode(self.email, forKey: .email)
        try container.encode(self.address, forKey: .address)
        try container.encode(self.about, forKey: .about)
        try container.encode(self.registered, forKey: .registered)
        try container.encode(self.tags, forKey: .tags)
        try container.encode(self.friends, forKey: .friends)
    }
    
    // 实现 Equatable 协议
    static func == (lhs: Friendface, rhs: Friendface) -> Bool {
        return lhs.id == rhs.id
    }
    
    // 实现 Hashable 协议
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
 }

上面的代码中,完整的定义了CodingKeys以及手动实现Encodable和Decodable,从而解决了@Model 属性包装器与 Codable 协议的实现有冲突的问题。

参考资料

1、Key points:https://www.hackingwithswift.com/guide/ios-swiftui/5/2/key-points

2、Swift Codable 深入解析:理解CodingKeys的关键角色:https://fangjunyu.com/2024/10/14/swift-codable-%e6%b7%b1%e5%85%a5%e8%a7%a3%e6%9e%90%ef%bc%9a%e7%90%86%e8%a7%a3-codingkeys-%e7%9a%84%e5%85%b3%e9%94%ae%e8%a7%92%e8%89%b2/

3、Swift为什么定义CodingKeys:https://fangjunyu.com/2024/11/12/swift%e4%b8%ba%e4%bb%80%e4%b9%88%e5%ae%9a%e4%b9%89codingkeys/

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注