提问人:JohnSF 提问时间:11/15/2023 最后编辑:JohnSF 更新时间:11/16/2023 访问量:189
JSON 到 SwiftData 的 JSON 解码在 ObservationRegistrar 以错误告终
JSON to SwiftData Decode of JSON Ends in Error at ObservationRegistrar
问:
我正在为适用于 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 的模型代码中:
任何指导将不胜感激。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)
}
}
}
答:
请尝试此方法,其中声明 和 .SDTopLevel
SDFuelStation
将 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)
}
}
该解决方案与我们在 Core Data 中所做的非常相似,我们需要将模型上下文传递给解码器,以便我们可以在 并插入顶级对象。init(from:)
为此,我们使用 JSONDecoder 上的字典,因此首先我们需要定义一个键userInfo
let modelContextKey = CodingUserInfoKey(rawValue: "modelcontext")!
然后,在解码之前,我们将变量 传递给解码器Environment
ModelContext
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
评论
context
评论
//await loadDataSD()//does not work
SDTopLevel
SDTopLevel