-1

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 :(")
        }
    }
}
1
  • 2
    Nowhere in your posted code have you implemented either geoDataArray or a ForEach. Also, all of those nested structs may be giving you issues. Remember, only GeoResult exists outside of GeoResult, so, if you are trying to use a location, you can't, say in ContentView declare let location: Location because it won't be in scope. If you try let location: GeoResult.Location you will get an error that it is not a member of GeoResult. Lastly, you haven't stated exactly what you want to do, other than iterate through an array of codable structs... and do what as a result? Commented May 19, 2022 at 19:15

1 Answer 1

1

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)")
        }
    }
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks so much! This is awesome, it works great. Your explanation was a life saver haha. Just curious, why does the value of formatted_address need to be checked every time?
@QuintonPrice: the approach above works because SwiftUI can differentiate one instance of GeoResult from another. When we say “I’d: \.self”, Swift hashes the instances and uses formatted_address for that (because I chose that property in the functions). If two instances have exactly the same formatted_address, SwiftUI can no longer identify them as two different items, and the behavior of the list could be unpredictable. It would be preferred to create an “id” property in GeoResult and have it conform to Identifiable instead of Hashable.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.