JSON 到 SwiftData 的 JSON 解码在 ObservationRegistrar 以错误告终

JSON to SwiftData Decode of JSON Ends in Error at ObservationRegistrar

提问人:JohnSF 提问时间:11/15/2023 最后编辑:JohnSF 更新时间:11/16/2023 访问量:189

问:

我正在为适用于 iOS17 和 SwiftData 的 SwiftUI 应用程序而苦苦挣扎。我正在尝试从 JSON 源下载数据并将数据存储在 SwiftData 中。我可以将数据下载并解码为 Swift 结构,但无法使用 SwiftData @Model类执行此操作。下面的代码包括 struct 和 SwiftData 过程。SwiftData 类是 SDTopLevel 和 SDFuelStation,结构是 TopLevelX 和 FuelStationX。FuelStationListView 中的按钮调用任一 loadDataSD 或 loadDataX。URL 正确且包含演示密钥。

struct FuelStationListView: View {

    @Environment(\.modelContext) var context
    @Query(sort: \SDFuelStation.stationName) var fuelStations: [SDFuelStation]

    @State private var sdTopLevel: SDTopLevel?
    @State private var topLevel: TopLevelX?

    var body: some View {
        NavigationStack {
            Button("Fetch") {
                Task {
                    await loadDataX()//works
                    //await loadDataSD()//does not work
                }
            }
            List {
                ForEach(fuelStations) { fuelStation in
                    Text(fuelStation.stationName)
                }
            }
            .navigationTitle("Fuel Stations")
        }//nav
    }//body

    func loadDataSD() async {

        guard let url = URL(string: "https://developer.nrel.gov/api/alt-fuel-stations/v1.json?api_key=DEMO_KEY&limit=10") else {
            print("Invalid URL")
            return
        }

        do {
            let (data, response) = try await URLSession.shared.data(from: url)
            guard (response as? HTTPURLResponse)?.statusCode == 200 else {
                print(response)
                return
            }
        
            let decoder = JSONDecoder()
            decoder.keyDecodingStrategy = .convertFromSnakeCase
            let decodedResponse = try decoder.decode(SDTopLevel.self, from: data)
            print(decodedResponse)
        
            sdTopLevel = decodedResponse
            print("sdTopLevel.fuelStations.count is \(sdTopLevel?.fuelStations.count ?? 1000)")

        } catch {
            print("Invalid Data")
        }

    }//load data

    func loadDataX() async {

        guard let url = URL(string: "https://developer.nrel.gov/api/alt-fuel-stations/v1.json?api_key=DEMO_KEY&limit=10") else {
            print("Invalid URL")
            return
        }

        do {
        
            let (data, response) = try await URLSession.shared.data(from: url)
            guard (response as? HTTPURLResponse)?.statusCode == 200 else {
                print(response)
                return
            }
        
            let decoder = JSONDecoder()
            decoder.keyDecodingStrategy = .convertFromSnakeCase
            let decodedResponse = try decoder.decode(TopLevelX.self, from: data)
        
            self.topLevel = decodedResponse
            print("topLevel.fuelStations.count is \(topLevel?.fuelStations.count ?? 0)")

            for station in decodedResponse.fuelStations {
                print(station.stationName)
            }
        
            self.topLevel = nil

        } catch {
            print("Invalid Data")
        }

    }//load data

}//struct fuel Station list view

和数据:

@Model
class SDFuelStation: Codable {

    enum CodingKeys: CodingKey {
        case id, city, stationName, streetAddress
    }//enum

    public var id: Int

    var stationName: String = ""
    var streetAddress: String = ""
    var city: String = ""

    public init(stationName: String, streetAddress: String, city: String) {
        self.id = 0
        self.stationName = stationName
        self.streetAddress = streetAddress
        self.city = city
    }

    required public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(Int.self, forKey: .id)
        stationName = try container.decode(String.self, forKey: .stationName)
        streetAddress = try container.decode(String.self, forKey: .streetAddress)
        city = try container.decode(String.self, forKey: .city)
    }//required init

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
    
        try container.encode(id, forKey: .id)
        try container.encode(stationName, forKey: .stationName)
        try container.encode(streetAddress, forKey: .streetAddress)
        try container.encode(city, forKey: .city)
    }

}//class

struct TopLevelX: Codable {
    let fuelStations: [FuelStationX]
}

struct FuelStationX: Codable {

    let id: Int

    var stationName: String = ""
    var streetAddress: String = ""
    var city: String = ""

}//struct

错误出在 getter 的模型代码中:

enter image description here

任何指导将不胜感激。Xcode 15.0 iOS的17

编辑: 我错误地错过了 SDTopLevel 类:

@Model
class SDTopLevel: Codable {

    enum CodingKeys: CodingKey {
        case fuelStations
    }

    var fuelStations: [SDFuelStation]

    init(fuelStations: [SDFuelStation]) {
        self.fuelStations = fuelStations
    }

    required public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        fuelStations = try container.decode([SDFuelStation].self, forKey: .fuelStations)
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(fuelStations, forKey: .fuelStations)
    }

}

var fuelStations 的代码展开如下,错误在行上:return self.getValue(forKey: .fuelStations),错误是“Thread 10: EXC_BREAKPOINT (code=1, subcode=0x1a8a5303c)”

{
    @storageRestrictions(accesses: _$backingData, initializes: _fuelStations)
        init(initialValue) {
                    _$backingData.setValue(forKey: \.fuelStations, to: initialValue)
                    _fuelStations = _SwiftDataNoType()
        }

    get {
                    _$observationRegistrar.access(self, keyPath: \.fuelStations)
                    return self.getValue(forKey: \.fuelStations)
        }

    set {
                    _$observationRegistrar.withMutation(of: self, keyPath: \.fuelStations) {
                            self.setValue(forKey: \.fuelStations, to: newValue)
                    }
        }
}
iOS Xcode SwiftUI swift-data

评论

1赞 workingdog support Ukraine 11/15/2023
您的图片不可读。以文本形式完整显示错误,并在代码的哪一行上显示错误。
0赞 workingdog support Ukraine 11/15/2023
在你的代码中,你说,这是你有错误的地方吗?是关于解码的错误,如果是这样,您可以显示您的代码。//await loadDataSD()//does not workSDTopLevelSDTopLevel
0赞 JohnSF 11/16/2023
上面添加了代码和说明。
0赞 JohnSF 11/16/2023
是的 - await loadDataSD 是尝试将 JSON 解码为 @Model 类的代码,这是不起作用的部分。
0赞 JohnSF 11/16/2023
我确实删除了插入命令 - 因为我没有走到那一步。也许你对直接解码的想法是一个线索。多年来,我一直在 Core Data 中解码和放置对象,但我相信在所有情况下,我都会解码为结构,然后创建 Core Data 对象。尽管如此,在我看来,我的问题是 JSON 根本没有解码到 SDTopLevel 中 - 我一定在某处有一个可解码错误。

答:

1赞 workingdog support Ukraine 11/16/2023 #1

请尝试此方法,其中声明 和 .SDTopLevelSDFuelStation

将 json 数据直接解码为模型类型效果很好, 如以下示例代码所示。

import SwiftUI
import SwiftData

@main
struct TestApp: App {
    var sharedModelContainer: ModelContainer = {
        let schema = Schema([SDTopLevel.self])
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
        do {
            return try ModelContainer(for: schema, configurations: [modelConfiguration])
        } catch {
            fatalError("Could not create ModelContainer: \(error)")
        }
    }()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(sharedModelContainer)
    }
}

struct ContentView: View {
    @Environment(\.modelContext) var context
    
    var body: some View {
        FuelStationListView()
    }
}

struct FuelStationListView: View {
    @Environment(\.modelContext) var context
    @Query(sort: \SDFuelStation.stationName) var fuelStations: [SDFuelStation]
 //   @Query() var sdTopLevel: [SDTopLevel]

    var body: some View {
        NavigationStack {
            Button("Fetch") {
                Task {
                    await loadDataSD()
                }
            }
//            List {
//                ForEach(sdTopLevel) { sd in
//                    ForEach(sd.fuelStations ?? []) { fuelStation in
//                        Text(fuelStation.stationName)
//                    }
//                }
//            }
            
            List {
                ForEach(fuelStations) { fuelStation in
                    Text(fuelStation.stationName)
                }
            }
            
            .navigationTitle("Fuel Stations")
        }//nav
    }//body
    
    func loadDataSD() async {
        guard let url = URL(string: "https://developer.nrel.gov/api/alt-fuel-stations/v1.json?api_key=DEMO_KEY&limit=10") else {
            print("Invalid URL")
            return
        }
        do {
            let (data, response) = try await URLSession.shared.data(from: url)
            guard (response as? HTTPURLResponse)?.statusCode == 200 else {
                print(response)
                return
            }
            let decoder = JSONDecoder()
            decoder.keyDecodingStrategy = .convertFromSnakeCase
            let decodedResponse = try decoder.decode(SDTopLevel.self, from: data)
            context.insert(decodedResponse)  // <--- here
        } catch {
            print("----> error: \(error)")
        }
    }
    
}

@Model
class SDFuelStation: Identifiable, Codable {  // <--- here
    var id: Int
    var stationName: String = ""
    var streetAddress: String = ""
    var city: String = ""
    
    enum CodingKeys: CodingKey {
        case id, city, stationName, streetAddress
    }
    
    @Relationship var sdTopLevel: SDTopLevel?  // <--- here
    
    required public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(Int.self, forKey: .id)
        stationName = try container.decode(String.self, forKey: .stationName)
        streetAddress = try container.decode(String.self, forKey: .streetAddress)
        city = try container.decode(String.self, forKey: .city)
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(stationName, forKey: .stationName)
        try container.encode(streetAddress, forKey: .streetAddress)
        try container.encode(city, forKey: .city)
    }
 
}

@Model
class SDTopLevel: Identifiable, Codable {  // <--- here
    let id = UUID()
    
    @Relationship(inverse: \SDFuelStation.sdTopLevel) 
    var fuelStations: [SDFuelStation]?  // <--- here
    
    enum CodingKeys: CodingKey {
        case fuelStations
    }

    required public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        fuelStations = try container.decode([SDFuelStation].self, forKey: .fuelStations)
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(fuelStations, forKey: .fuelStations)
    }
    
}
3赞 Joakim Danielson 11/16/2023 #2

该解决方案与我们在 Core Data 中所做的非常相似,我们需要将模型上下文传递给解码器,以便我们可以在 并插入顶级对象。init(from:)

为此,我们使用 JSONDecoder 上的字典,因此首先我们需要定义一个键userInfo

let modelContextKey = CodingUserInfoKey(rawValue: "modelcontext")!

然后,在解码之前,我们将变量 传递给解码器EnvironmentModelContext

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.userInfo[modelContextKey] = context

然后我们读取这个键,并在作为 json 根对象的模型中使用它init(from:)

@Model
class SDTopLevel: Codable {
    //...

    required public init(from decoder: Decoder) throws {
        guard let context = decoder.userInfo[CodingUserInfoKey(rawValue: "modelcontext")!] as? ModelContext else {
            fatalError() // replace with throw some error
        }
        let container = try decoder.container(keyedBy: CodingKeys.self)
        fuelStations = try container.decode([SDFuelStation].self, forKey: .fuelStations)
        context.insert(self)
    }

我们不需要做同样的事情,因为如果关系的一方已经插入到模型上下文中,那么对于 SwiftData 来说就足够了。SDFuelStation

评论

0赞 JohnSF 11/18/2023
谢谢!第一次尝试 - 这对我有用。我会做一些测试。
0赞 Van Du Tran 12/6/2023
@Joakim Danielson,我不确定前两个代码的去向?“让 modelContextKey....”和“......decoder.userInfo[modelContextKey] = context”。“上下文”在哪里定义?
0赞 Joakim Danielson 12/6/2023
第一个应该是全局的,因此可以从两个位置访问它。 在视图中定义,请参阅问题中的第一个代码部分,您需要将其替换为您进行解码的任何模型上下文。context