I’m struggling with a SwiftUI app for iOS17 and SwiftData. I am attempting to download data from a JSON source and store the data in SwiftData. I can download and decode the data into a Swift struct, but have not been able to do so with a SwiftData @Model class. The code below includes both the struct and SwiftData procedures. The SwiftData classes are SDTopLevel and SDFuelStation, the structs are TopLevelX and FuelStationX. The button in FuelStationListView calls either
loadDataSD or loadDataX. The URL is correct and contains a demo key.
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
And the data:
@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
The error is in the Model code in a getter:
Any guidance would be appreciated. Xcode 15.0 iOS 17
EDIT:
I mistakenly missed the SDTopLevel class:
@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)
}
}
The code for var fuelStations unfolds as the following and the error is on the line: return self.getValue(forKey: .fuelStations) and the error is "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)
}
}
}
2
Answers
Try this approach, where you declare a relationship between the
SDTopLevel
and theSDFuelStation
.Decoding json data directly into model type works very well,
as shown in this example code.
The solution is pretty similar to what we do in Core Data, we need to pass the model context to the decoder so we can use it in the
init(from:)
and insert the top level object.We are using the
userInfo
dictionary on the JSONDecoder for this so first we need to define a keyThen before decoding we pass our
Environment
variable forModelContext
to the decoderThen we read this key and use it in the
init(from:)
for the model that is the root object for the jsonWe do not need to do the same for
SDFuelStation
because it is enough for SwiftData if one side of the relationship has been inserted into a model context.