问题描述
在学习Codable将数据转换为JSON串并保存在UserDefaults时,发现每次都无法加载数据,怀疑是UserDefaults哪个流程出错了。
问题代码为:
struct ExpenseItem: Identifiable, Codable {
var id = UUID()
let name: String
let type: String
let amount: Double
}
@Observable
class Expenses: Codable {
var items = [ExpenseItem]()
}
@State private var expenses = Expenses() {
didSet {
UserDefaults.standard.set(try? JSONEncoder().encode(expenses.items), forKey: "Items")
print("输出完成")
}
}
...
.toolbar {
Button("Save") {
let item = ExpenseItem(name: name, type: type, amount: amount)
expenses.items.append(item)
UserDefaults.standard.set(try? JSONEncoder().encode(expenses), forKey: "Items")
if let loadData = UserDefaults.standard.data(forKey: "Items") {
if let loadItem = try? JSONDecoder().decode([ExpenseItem].self, from: loadData) {
print("加载成功")
}
}
dismiss()
}
上面的代码是,我在录入数据(name、type和amount)后,将数据保存在ExpenseItem格式中,然后将保存后的数据追加到Expenses类的示例中。并将Expenses类的示例存储到UserDefaults当中。
数据存储后,重新打开应用时,重新将数据从UserDefaults读取。
但我在实际测试中发现重新打开应用时,数据并没有重新从UserDefaults中读取,因此,问题出在UserDefaults的存储或读取当中。
于是,我计划在UserDefaults存储后,马上进行UserDefaults的读取工作,并修改为下面的代码:
.toolbar {
Button("Save") {
let item = ExpenseItem(name: name, type: type, amount: amount)
expenses.items.append(item)
UserDefaults.standard.set(try? JSONEncoder().encode(expenses), forKey: "Items")
print("保存成功!")
if let loadData = UserDefaults.standard.data(forKey: "Items") {
do{
let loadItem = try JSONDecoder().decode([ExpenseItem].self, from: loadData)
print("保存成功并加载成功!")
print("返回的userDefaults为:\(loadItem)")
} catch {
print("加载出错了!")
}
}
dismiss()
}
在模拟器上运行后,发现输出的内容为:
expenses.item:[hackingwithswift.ExpenseItem(id: 037A5634-C9C5-4DF7-9EE0-82A0280C790D, name: "fang", type: "some", amount: 1.11), hackingwithswift.ExpenseItem(id: C88A4856-7992-4835-AFBA-06F5B92FDC58, name: "12", type: "Personal", amount: 0.0), hackingwithswift.ExpenseItem(id: D33DDD3B-B749-4CD5-AFEA-367D264D2F67, name: "112", type: "Personal", amount: 0.0)]
保存成功!
加载出错了!
这也说明存储或者加载UserDefaults存在问题。经过排查后发现,实际的问题是我在UserDefaults存储的格式有问题。
错误的存储方式:
UserDefaults.standard.set(try? JSONEncoder().encode(expenses), forKey: "Items")
正确的存储方式:
UserDefaults.standard.set(try? JSONEncoder().encode(expenses.items), forKey: "Items")
修改为expense.items后,UserDefaults就可以正常的解码了,模拟器输出:
保存成功!
保存成功并加载成功!
返回的userDefaults为:[hackingwithswift.ExpenseItem(id: E3E43E5F-A030-4733-8C47-612116D65BAF, name: "112", type: "Personal", amount: 0.0)]
问题分析
这个问题的实际报错原因是,我在UserDefaults中存储的是expenses对象:
@Observable
class Expenses: Codable {
var items = [ExpenseItem]()
}
@State private var expenses = Expenses() {
didSet {
UserDefaults.standard.set(try? JSONEncoder().encode(expenses.items), forKey: "Items")
print("输出完成")
}
}
但是我实际使用JSONDecoder解析的格式为ExpenseItem:
let loadItem = try JSONDecoder().decode([ExpenseItem].self, from: loadData)
这里我用ABC作为对应逻辑关系:
- 使用JSONEncoder编码的expenses(A)是一个class类型的Expenses实例
- class类型的Expenses实例(A)包含的是名为items的ExpenseItem类型数组(B)
- 我使用expenses这个class类型(A)编码,用ExpenseItem类型(B)解码,两者的类型不同,因此报错。
所以,解决方案就用expenses.item(ExpenseItem类型数组,B)编码,在用[ExpenseItem].self(ExpenseItem类型结构,B)解码。
这样,数据就能够按照要求对应格式进行编码和解码。
换句话说,我们也可以用expenses这个class类型(A)编码,用Expenses.self(A)解码:
let item = ExpenseItem(name: name, type: type, amount: amount)
expenses.items.append(item)
UserDefaults.standard.set(try?JSONEncoder().encode(expenses), forKey: "Items")
print("保存成功!")
if let loadData = UserDefaults.standard.data(forKey: "Items") {
do {
let loadItem = try JSONDecoder().decode(Expenses.self, from: loadData)
print("保存成功并加载成功!")
print("loadItem为:\(loadItem)")
print("loadItem.item为:\(loadItem.items)")
} catch {
print("加载出错了!")
}
}
模拟器输出为:
loadItem为:hackingwithswift.Expenses
loadItem.item为:[hackingwithswift.ExpenseItem(id: 4082E43A-AFBD-4174-B5AF-987ED80B7E63, name: "1234", type: "Personal", amount: 0.0)]
因此,当你进行编码和解码时,一定要注意编解码的类型一直,否则就会存在UserDefaults无法使用的问题,此外还需要注意,如果无法排查问题,就使用do-catch语法排查并捕获对应的错误。
最后,当你使用expenses对象编解码时,在应用的读取时,也要设置读取的expenses对象的items元素赋值给现有expenses对象的items元素。
init() {
print("读取UserDefaults的数据")
if let loadData = UserDefaults.standard.data(forKey: "Items") {
do {
let loadItem = try JSONDecoder().decode(Expenses.self, from: loadData)
expenses.items = loadItem.items
print("数据解码后赋值给items。")
return
} catch {
print("加载出错了!")
}
}
print("当前没有数据并置空数组")
expenses.items = []
}