2

我对 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 数组相同的方法,它完美地工作,所以我的自定义类似乎存在问题。

4

2 回答 2

1

通过使用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 查看模型版本(需要相同的卡片和上面的数组扩展):


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)")
                }
            }
        }
    }
}

你原来的 @Published 实现依赖于一个cancellableSet似乎不存在的,所以我把它换成了一个普通的 @Published 值,它带有一个初始化器,它接受 UserDefaults 值,然后再次设置 UserDefaultsdidSet

于 2021-03-28T21:43:26.850 回答
0

我认为您可以像这样从模型中制作一个简单的结构:

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 = ""

于 2021-03-28T21:47:45.240 回答