Home > front end >  Iterating over array of Codable in SwiftUI
Iterating over array of Codable in SwiftUI

Time:05-20

As a follow-up to this question, I now want to iterate through an array of Codable structs in SwiftUI and render them in my ContentView{} as Text or List items.

I have tried implementing a variable, geoDataArray, in the .task section then iterating over it with a ForEach in my ContentView but received a lot of errors about types and unwrapping values.

Any help is appreciated! I am still new to SwiftUI.

Below is my code:

struct GeoService: Codable {
    var status: String
    var results: [GeoResult]
}

struct GeoResult: Codable {
    
    struct Geometry: Codable {
        
        struct Location: Codable {
            
            let lat: Float
            let lng: Float
            
            init() {
                lat = 32
                lng = 30
            }
        }
        let location: Location
    }
    let formatted_address: String
    let geometry: Geometry
}



struct ContentView: View {

//    @State private var results: Any ?????????
    
    var body: some View {
        NavigationView {
            Text("Test")
                .navigationTitle("Quotes")
                .task {
                    await handleData()
                }
        }
        
    }
    
    func handleData() async {
        let geoResult="""
        {
          "results": [
            {
              "formatted_address": "1600 Amphitheatre Parkway, Mountain View, CA 94043, USA",
              "geometry": {
                "location": {
                  "lat": 37.4224764,
                  "lng": -122.0842499
                }
              }
            },
            {
              "formatted_address": "Test addresss",
              "geometry": {
                "location": {
                  "lat": 120.32132145,
                  "lng": -43.90235469
                }
              }
            }
          ],
          "status": "OK"
        }
        """.data(using: .utf8)!
        
        let decoder = JSONDecoder()
        print("executing handleData()")
        do {
            let obj = try decoder.decode(GeoService.self, from: geoResult)
            for result in obj.results {
                print("Address: \(result.formatted_address)")
                print("Lat/long: (\(result.geometry.location.lat), \(result.geometry.location.lng))")
            }
        } catch {
            print("Did not work :(")
        }
    }
}

CodePudding user response:

Your code works fine the way it is for printing to the console, but ForEach requires that GeoResult conforms to either Identifiable (preferred) or at least Hashable. Given that you didn't include the property id in your code, let's have that struct conforming to Hashable.

So, assuming that each GeoResult is different because formatted_address is never the same (you must check if that's true), you can add two functions to ensure conformance. You will get the following:

struct GeoResult: Codable, Hashable {    // <- Conform to Hashable
    
    // Differentiating
    static func == (lhs: GeoResult, rhs: GeoResult) -> Bool {
        lhs.formatted_address == rhs.formatted_address
    }

    // Hashing
    func hash(into hasher: inout Hasher) {
        hasher.combine(formatted_address)
    }
    
    
    struct Geometry: Codable {
        
        struct Location: Codable {
            
            let lat: Float
            let lng: Float
            
            init() {
                lat = 32
                lng = 30
            }
        }
        let location: Location
    }
    let formatted_address: String
    let geometry: Geometry
}

In the view, add an array of GeoResult, that will be the @State variable to iterate over. Place the .task() modifier on the outermost view.

    // This is the list
    @State private var geoArray: [GeoResult] = []
    
    var body: some View {
        NavigationView {
            VStack {
                
                // GeoResult is not Identifiable, so it is necessary to include id: \.self
                ForEach(geoArray, id: \.self) { result in
                    NavigationLink {
                        Text("Lat/long: (\(result.geometry.location.lat), \(result.geometry.location.lng))")
                    } label: {
                        Text("Address: \(result.formatted_address)")
                    }
                }
                .navigationTitle("Quotes")
            }
        }
        
        // Attach the task to the outermost view, in this case the NavigationView
        .task {
            await handleData()
        }
    }

Finally, change the @State variable in your function, after decoding:

    func handleData() async {

        // ...
        
        let decoder = JSONDecoder()
        do {
            let obj = try decoder.decode(GeoService.self, from: geoResult)
            
            // Add this
            geoArray = obj.results
        } catch {
            print("Did not work :(\n\(error)")
        }
    }
  • Related