将 @AppStoarge 与自定义对象数组一起使用不会保留数据

svc*_*cho 3 class swift swiftui appstorage

我对 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)
  }
}
Run Code Online (Sandbox Code Playgroud)

这是我代表该对象的类:

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
}
Run Code Online (Sandbox Code Playgroud)

我正在使用一个视图模型类,它声明数组如下:

@Published(wrappedValue: [], "saved_cards") var savedCards: [Card]
Run Code Online (Sandbox Code Playgroud)

该类的其余部分仅包含将卡片附加到数组的函数,因此我认为没有必要在这里突出显示它们。

我的问题是,在应用程序运行时,一切似乎都工作正常 - 卡片出现并在数组中可见,但是当我尝试关闭我的应用程序并再次重新打开它时,数组为空,并且数据似乎没有持久化。看起来 JSONEncoder/Decoder 无法序列化/反序列化我的类,但我不明白为什么。

我真的很感激建议,因为我似乎找不到解决这个问题的方法。我还对常规 Int 数组使用相同的方法,该数组工作完美,因此看来我的自定义类存在问题。

jn_*_*pdx 6

By using try? JSONDecoder().decode(Value.self, from: data) and not do/try/catch you're missing the error that's happening in the JSON decoding. The encoder isn't putting in the legality key, so the decoding is failing to work. In fact, all of your types on Codable by default, so if you remove all of your custom Codable encode/decode and let the compiler synthesize it for you, it encodes/decodes just fine.

Example with @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)")
                }
            }
        }
    }
}

Run Code Online (Sandbox Code Playgroud)

View model version with @Published (requires same Card, and Array extension from above):


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, name: "\(Date())", description: "", legality: [], imageURL: "", price: ""))
            }
            List {
                ForEach(viewModel.savedCards, id: \.id) { card in
                    Text("\(card.name)")
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Your original @Published implementation relied on a cancellableSet that didn't seem to exist, so I switched it out for a regular @Published value with an initializer that takes the UserDefaults value and then sets UserDefaults again on didSet