编码为 JSON 格式不会对 Swift 中的切换布尔值进行编码

Encoding to JSON format is not encoding the toggled boolean value in Swift

我正在制作一个应用程序,其中包含有关不同木材、药草和香料以及其他一些信息的信息。我包括将他们最喜欢的项目保存到收藏夹列表的功能,因此我有一个心形按钮,用户可以按下该按钮将其添加到收藏夹。按下按钮会切换项目的 isFavorite 属性,然后离开页面会调用对数据进行编码以将其保存到用户设备的方法。我遇到的问题 运行 是它没有对 isFavorite 属性 的更新值进行编码。它仍然将该值编码为 false,因此收藏夹列表在关闭并重新打开应用程序后不会保留。

这是我的 Wood.swift 代码,该文件设置了木材项目的结构。我还包含了我用来确保它在 Wood 扩展中正确显示的测试数据:

import Foundation

struct Wood: Identifiable, Codable {
    var id = UUID()
    
    var mainInformation: WoodMainInformation
    var preparation: [Preparation]
    var isFavorite = false
    
    init(mainInformation: WoodMainInformation, preparation: [Preparation]) {
        self.mainInformation = mainInformation
        self.preparation = preparation
    }
}

struct WoodMainInformation: Codable {    
    var category: WoodCategory
    var description: String
    var medicinalUses: [String]
    var magicalUses: [String]
    var growZone: [String]
    var lightLevel: String
    var moistureLevel: String
    var isPerennial: Bool
    var isEdible: Bool
}

enum WoodCategory: String, CaseIterable, Codable {
    case oak = "Oak"
    case pine = "Pine"
    case cedar = "Cedar"
    case ash = "Ash"
    case rowan = "Rowan"
    case willow = "Willow"
    case birch = "Birch"
}

enum Preparation: String, Codable {
    case talisman = "Talisman"
    case satchet = "Satchet"
    case tincture = "Tincture"
    case salve = "Salve"
    case tea = "Tea"
    case ointment = "Ointment"
    case incense = "Incense"
}

extension Wood {
    static let woodTypes: [Wood] = [
        Wood(mainInformation: WoodMainInformation(category: .oak,
                                                  description: "A type of wood",
                                                  medicinalUses: ["Healthy", "Killer"],
                                                  magicalUses: ["Spells", "Other Witchy Stuff"],
                                                  growZone: ["6A", "5B"],
                                                  lightLevel: "Full Sun",
                                                  moistureLevel: "Once a day",
                                                  isPerennial: false,
                                                  isEdible: true),
             preparation: [Preparation.incense, Preparation.satchet]),
        Wood(mainInformation: WoodMainInformation(category: .pine,
                                                  description: "Another type of wood",
                                                  medicinalUses: ["Healthy"],
                                                  magicalUses: ["Spells"],
                                                  growZone: ["11G", "14F"],
                                                  lightLevel: "Full Moon",
                                                  moistureLevel: "Twice an hour",
                                                  isPerennial: true,
                                                  isEdible: true),
             preparation: [Preparation.incense, Preparation.satchet])
    ]
}

这是我的 WoodData.swift 文件,该文件包含允许应用程序在木材列表中显示正确木材以及对木材进行编码和解码的方法:

import Foundation

class WoodData: ObservableObject {
    @Published var woods = Wood.woodTypes
    
    var favoriteWoods: [Wood] {
        woods.filter { [=13=].isFavorite }
    }
    
    func woods(for category: WoodCategory) -> [Wood] {
        var filteredWoods = [Wood]()
        
        for wood in woods {
            if wood.mainInformation.category == category {
                filteredWoods.append(wood)
            }
        }
        return filteredWoods
    }
    
    func woods(for category: [WoodCategory]) -> [Wood] {
        var filteredWoods = [Wood]()
        filteredWoods = woods
        
        return filteredWoods
    }
    
    func index(of wood: Wood) -> Int? {
        for i in woods.indices {
            if woods[i].id == wood.id {
                return i
            }
        }
        return nil
    }
    
    private var dataFileURL: URL {
        do {
            let documentsDirectory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
            return documentsDirectory.appendingPathComponent("evergreenData")
        }
        catch {
            fatalError("An error occurred while getting the url: \(error)")
        }
    }

    func saveWoods() {
        if let encodedData = try? JSONEncoder().encode(woods) {
            do {
                try encodedData.write(to: dataFileURL)
                let string = String(data: encodedData, encoding: .utf8)
                print(string)
            }
            catch {
                fatalError("An error occurred while saving woods: \(error)")
            }
        }

    }

    func loadWoods() {
        guard let data = try? Data(contentsOf: dataFileURL) else { return }
        do {
            let savedWoods = try JSONDecoder().decode([Wood].self, from: data)
            woods = savedWoods
        }
        catch {
            fatalError("An error occurred while loading woods: \(error)")
        }
    }
}

最后,这是我的 WoodsDetailView.swift 文件,该文件显示了所选木材的信息,并调用了对木材数据进行编码的方法:

import SwiftUI

struct WoodsDetailView: View {
    @Binding var wood: Wood
    
    @State private var woodsData = WoodData()

    var body: some View {
        VStack {
            List {
                Section(header: Text("Description")) {
                    Text(wood.mainInformation.description)
                }
                Section(header: Text("Preparation Techniques")) {
                    ForEach(wood.preparation, id: \.self) { technique in
                        Text(technique.rawValue)
                    }
                }
                Section(header: Text("Edible?")) {
                    if wood.mainInformation.isEdible {
                        Text("Edible")
                    }
                    else {
                        Text("Not Edible")
                    }
                }
                Section(header: Text("Medicinal Uses")) {
                    ForEach(wood.mainInformation.medicinalUses.indices, id: \.self) { index in
                        let medicinalUse = wood.mainInformation.medicinalUses[index]
                        Text(medicinalUse)
                    }
                }
                Section(header: Text("Magical Uses")) {
                    ForEach(wood.mainInformation.magicalUses.indices, id: \.self) { index in
                        let magicalUse = wood.mainInformation.magicalUses[index]
                        Text(magicalUse)
                    }
                }
                Section(header: Text("Grow Zone")) {
                    ForEach(wood.mainInformation.growZone.indices, id: \.self) { index in
                        let zone = wood.mainInformation.growZone[index]
                        Text(zone)
                    }
                }
                Section(header: Text("Grow It Yourself")) {
                    Text("Water: \(wood.mainInformation.moistureLevel)")
                    Text("Needs: \(wood.mainInformation.lightLevel)")
                    
                    if wood.mainInformation.isPerennial {
                        Text("Perennial")
                    }
                    else {
                        Text("Annual")
                    }
                }
            }
        }
        .navigationTitle(wood.mainInformation.category.rawValue)
        .onDisappear {
          woodsData.saveWoods()
        }
        .toolbar {
            ToolbarItem {
                HStack {
                    Button(action: {
                        wood.isFavorite.toggle()
                    }) {
                        Image(systemName: wood.isFavorite ? "heart.fill" : "heart")
                    }
                }
            }
        }
    }
}

struct WoodsDetailView_Previews: PreviewProvider {
    @State static var wood = Wood.woodTypes[0]
    
    static var previews: some View {
        WoodsDetailView(wood: $wood)
    }
}

这是我的 MainTabView.swift 文件:

import SwiftUI

struct MainTabView: View {
    @StateObject var woodData = WoodData()
            
    var body: some View {
        TabView {
            NavigationView {
                List {
                    WoodsListView(viewStyle: .allCategories(WoodCategory.allCases))
                }
            }
                .tabItem { Label("Main", systemImage: "list.dash")}
            NavigationView {
                    List {
                            WoodsListView(viewStyle: .favorites)

                    }

                .navigationTitle("Favorites")
            }.tabItem { Label("Favorites", systemImage: "heart.fill")}
        }
        .environmentObject(woodData)
        .onAppear {
            woodData.loadWoods()
        }
        .preferredColorScheme(.dark)
        
    }
}

struct MainTabView_Previews: PreviewProvider {
    static var previews: some View {
        MainTabView()
    }
}

这是我的 WoodListView.swift 文件:

import SwiftUI

struct WoodsListView: View {
    @EnvironmentObject private var woodData: WoodData
    let viewStyle: ViewStyle
    
    var body: some View {
        ForEach(woods) { wood in
            NavigationLink(wood.mainInformation.category.rawValue, destination: WoodsDetailView(wood: binding(for: wood)))
        }
    }
}

extension WoodsListView {
    enum ViewStyle {
        case favorites
        case singleCategory(WoodCategory)
        case allCategories([WoodCategory])
    }
    
    private var woods: [Wood] {
        switch viewStyle {
        case let .singleCategory(category):
            return woodData.woods(for: category)
        case let  .allCategories(category):
            return woodData.woods(for: category)
        case .favorites:
            return woodData.favoriteWoods
        }
    }
    
    func binding(for wood: Wood) -> Binding<Wood> {
        guard let index = woodData.index(of: wood) else {
            fatalError("Wood not found")
        }
        return $woodData.woods[index]
    }
}

struct WoodsListView_Previews: PreviewProvider {
    static var previews: some View {
        WoodsListView(viewStyle: .singleCategory(.ash))
            .environmentObject(WoodData())
    }
}

任何关于为什么不对切换的 isFavorite 属性 进行编码的帮助将不胜感激。

你的问题是结构是值类型 in Swift。从本质上讲,这意味着 WoodsDetailView 中的 Wood 实例与模型数组中的实例 (WoodData) 不同;它是一个副本(从技术上讲,副本是在您修改 isFavourite 属性 后立即创建的)。

在 SwiftUI 中,保持视图和模型之间的职责分离很重要。

更改 Wood 的收藏状态是视图应该要求模型执行的操作。

这是您遇到第二个问题的地方;在您的详细视图中,您正在创建模型的单独实例;您需要引用单个实例。

你有一个好的开始;您已将模型实例放在视图可以访问它的环境中。

首先,更改详细视图以删除绑定,从环境中引用模型并要求模型完成工作:

struct WoodsDetailView: View {
    var wood: Wood
    
    @EnvironmentObject private var woodsData: WoodData
    
    var body: some View {
        VStack {
            List {
                Section(header: Text("Description")) {
                    Text(wood.mainInformation.description)
                }
                Section(header: Text("Preparation Techniques")) {
                    ForEach(wood.preparation, id: \.self) { technique in
                        Text(technique.rawValue)
                    }
                }
                Section(header: Text("Edible?")) {
                    if wood.mainInformation.isEdible {
                        Text("Edible")
                    }
                    else {
                        Text("Not Edible")
                    }
                }
                Section(header: Text("Medicinal Uses")) {
                    ForEach(wood.mainInformation.medicinalUses, id: \.self) { medicinalUse in
                        Text(medicinalUse)
                    }
                }
                Section(header: Text("Magical Uses")) {
                    ForEach(wood.mainInformation.magicalUses, id: \.self) { magicalUse in
                        Text(magicalUse)
                    }
                }
                Section(header: Text("Grow Zone")) {
                    ForEach(wood.mainInformation.growZone, id: \.self) { zone in
                        Text(zone)
                    }
                }
                Section(header: Text("Grow It Yourself")) {
                    Text("Water: \(wood.mainInformation.moistureLevel)")
                    Text("Needs: \(wood.mainInformation.lightLevel)")
                    
                    if wood.mainInformation.isPerennial {
                        Text("Perennial")
                    }
                    else {
                        Text("Annual")
                    }
                }
            }
        }
        .navigationTitle(wood.mainInformation.category.rawValue)
        .onDisappear {
            woodsData.saveWoods()
        }
        .toolbar {
            ToolbarItem {
                HStack {
                    Button(action: {
                        self.woodsData.toggleFavorite(for: wood)
                    }) {
                        Image(systemName: wood.isFavorite ? "heart.fill" : "heart")
                    }
                }
            }
        }
    }
}

struct WoodsDetailView_Previews: PreviewProvider {
    static var wood = Wood.woodTypes[0]
    
    static var previews: some View {
        WoodsDetailView(wood: wood)
    }
}

我还摆脱了在列出属性时不必要地使用索引。

现在,将 toggleFavorite 函数添加到您的 WoodData object:

func toggleFavorite(for wood: Wood) {
     guard let index = self.woods.firstIndex(where:{ [=11=].id == wood.id }) else {
         return
     }
        
     self.woods[index].isFavorite.toggle()
}

您还可以删除 index(of wood:Wood) 函数(实际上只是复制 Array 的 firstIndex(where:) 函数)和 binding(for wood:Wood) 函数。

现在,您的代码不仅可以满足您的需求,而且还隐藏了从视图中切换收藏夹的机制;它只是要求切换最喜欢的状态,不需要知道这实际涉及什么。