Swift UI UserDefaults无法解码问题
Swift UI UserDefaults无法解码问题

Swift UI UserDefaults无法解码问题

问题描述

在学习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作为对应逻辑关系:

  1. 使用JSONEncoder编码的expenses(A)是一个class类型的Expenses实例
  2. class类型的Expenses实例(A)包含的是名为items的ExpenseItem类型数组(B)
  3. 我使用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 = []
}

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

发表回复

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