skip to Main Content

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:

enter image description here

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


  1. Try this approach, where you declare a relationship between the SDTopLevel and the SDFuelStation.

    Decoding json data directly into model type works very well,
    as shown in this example code.

    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)
        }
        
    }
    
    Login or Signup to reply.
  2. 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 key

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

    Then before decoding we pass our Environment variable for ModelContext to the decoder

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

    Then we read this key and use it in the init(from:) for the model that is the root object for the json

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

    We 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search