问题描述
我是 SwiftUI 的新手,想使用 @AppStorage 属性包装器以数组的形式保留自定义类对象列表。我在这里找到了几篇文章,它们帮助我创建了以下通用扩展,并将其添加到我的 AppDelegate 中:
extension Published where Value: Codable {
init(wrappedValue defaultValue: Value,_ key: String,store: UserDefaults? = nil) {
let _store: UserDefaults = store ?? .standard
if
let data = _store.data(forKey: key),let value = try? JSONDecoder().decode(Value.self,from: data) {
self.init(initialValue: value)
} else {
self.init(initialValue: defaultValue)
}
projectedValue
.sink { newValue in
let data = try? JSONEncoder().encode(newValue)
_store.set(data,forKey: key)
}
.store(in: &cancellableSet)
}
}
这是我代表对象的类:
class Card: ObservableObject,Identifiable,Codable{
let id : Int
let name : String
let description : String
let legality : [String]
let imageURL : String
let price : String
required init(from decoder: Decoder) throws{
let container = try decoder.container(keyedBy: CardKeys.self)
id = try container.decode(Int.self,forKey: .id)
name = try container.decode(String.self,forKey: .name)
description = try container.decode(String.self,forKey: .description)
legality = try container.decode([String].self,forKey: .legality)
imageURL = try container.decode(String.self,forKey: .imageURL)
price = try container.decode(String.self,forKey: .price)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CardKeys.self)
try container.encode(id,forKey: .id)
try container.encode(name,forKey: .name)
try container.encode(description,forKey: .description)
try container.encode(imageURL,forKey: .imageURL)
try container.encode(price,forKey: .price)
}
init(id: Int,name: String,description: String,legality: [String],imageURL: String,price : String) {
self.id = id
self.name = name
self.description = description
self.legality = legality
self.imageURL = imageURL
self.price = price
}
}
enum CardKeys: CodingKey{
case id
case name
case description
case legality
case imageURL
case price
}
我正在使用一个视图模型类,它声明数组如下:
@Published(wrappedValue: [],"saved_cards") var savedCards: [Card]
该类的其余部分仅包含将卡片附加到数组的函数,因此我认为没有必要在此处突出显示。
我的问题是,在应用程序运行期间,一切似乎都运行良好 - 卡片出现并在数组中可见,但是当我尝试关闭我的应用程序并再次重新打开它时,数组是空的,并且似乎是数据没有坚持。看起来 JSONEncoder/Decoder 无法序列化/反序列化我的类,但我不明白为什么。
我真的很感激建议,因为我似乎没有找到解决这个问题的方法。我也对常规 Int 数组使用相同的方法,该数组可以完美运行,因此我的自定义类似乎存在问题。
解决方法
通过使用 try? JSONDecoder().decode(Value.self,from: data)
而不是 do/try/catch
,您会错过 JSON 解码中发生的错误。编码器未输入 legality
键,因此解码无法正常工作。事实上,默认情况下 Codable
上的所有类型,因此如果您删除所有自定义 Codable 编码/解码并让编译器为您合成它,它编码/解码就好了。
@AppStorage 示例:
struct Card: Identifiable,Codable{
let id : Int
let name : String
let description : String
let legality : [String]
let imageURL : String
let price : String
init(id: Int,name: String,description: String,legality: [String],imageURL: String,price : String) {
self.id = id
self.name = name
self.description = description
self.legality = legality
self.imageURL = imageURL
self.price = price
}
}
enum CardKeys: CodingKey{
case id
case name
case description
case legality
case imageURL
case price
}
extension Array: RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8) else {
return nil
}
do {
let result = try JSONDecoder().decode([Element].self,from: data)
print("Init from result: \(result)")
self = result
} catch {
print("Error: \(error)")
return nil
}
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),let result = String(data: data,encoding: .utf8)
else {
return "[]"
}
print("Returning \(result)")
return result
}
}
struct ContentView : View {
@AppStorage("saved_cards") var savedCards : [Card] = []
var body: some View {
VStack {
Button("add card") {
savedCards.append(Card(id: savedCards.count + 1,name: "\(Date())",description: "",legality: [],imageURL: "",price: ""))
}
List {
ForEach(savedCards,id: \.id) { card in
Text("\(card.name)")
}
}
}
}
}
使用@Published 查看模型版本(需要与上面相同的 Card 和 Array 扩展):
class CardViewModel: ObservableObject {
@Published var savedCards : [Card] = Array<Card>(rawValue: UserDefaults.standard.string(forKey: "saved_cards") ?? "[]") ?? [] {
didSet {
UserDefaults.standard.setValue(savedCards.rawValue,forKey: "saved_cards")
}
}
}
struct ContentView : View {
@StateObject private var viewModel = CardViewModel()
var body: some View {
VStack {
Button("add card") {
viewModel.savedCards.append(Card(id: viewModel.savedCards.count + 1,price: ""))
}
List {
ForEach(viewModel.savedCards,id: \.id) { card in
Text("\(card.name)")
}
}
}
}
}
您最初的@Published 实现依赖于一个似乎不存在的 cancellableSet
,所以我将它切换为常规的 @Published 值,并使用一个初始值设定项,该初始值设定项取 UserDefaults 值,然后在 didSet
上再次设置 UserDefaults {1}}
我认为您可以像这样从模型中创建一个简单的结构:
struct Card : Codable,Identifiable{
let id : Int
let name : String
let description : String
let legality : [String]
let imageURL : String
let price : String
enum CodingKeys: String,CodingKey {
case id
case name
case description
case legality
case imageURL
case price
}
}
然后使用 jsonEncoder 像这样从你的数据中获取一个 json 字符串
// Encode
let card = Card(id: 1,name: "AAA",description: "BBB" ....)
let jsonEncoder = JSONEncoder()
let jsonData = try jsonEncoder.encode(card)
let json = String(data: jsonData,encoding: String.Encoding.utf8)
然后简单地使用:
UserDefaults.standard.set(json,forKey: "yourData")
当你想在 UI 中观察和使用它时,你可以使用:
@AppStorage("yourData") var myArray: String = ""