0

When trying to parse some data from a REST API endpoint in SwiftUI, I'm struggling to render out to a list correctly.

In the below example, my List requires the exact array position to render results, so it won't show anything without [0] etc...

I'm used to solving this type of issue in JavaScript using a for loop and iterating over it to assign each array position to [i] however in SwiftUI I'm struggling to do the same.

I think it's likely an issue with how I'm handling the nested JSON objects but here we go. I'm also confident it's probably an easy solution that I'm just blanking on here.

I've created a test endpoint that you can fetch from if you need to run it locally (this is in the below examples already).

To clarify what I'm looking to do here. I want to iterate over the array to reference (for example) Text(item.objects.title) without having to specify which item in the array I'm looking for. Using a ForEach doesn't work sadly because the List(viewModel.items, id: \.self) { item in } is essentially replicating the ForEach looping and doesn't work either.

Model

struct ObjectResponse: Hashable, Decodable {
    let objects: [Object]
}

struct Object: Hashable, Decodable {
    let slug: String
    let title: String
    let metadata: Metadata
}

struct Metadata: Hashable, Decodable {
    let published: String
    let url: String
    let snippet: String
    let read: Bool
}

ViewModel


class ContentViewModel: ObservableObject {
    @Published var items: [ObjectResponse] = []
    func fetchData() {
        let api = "https://api.cosmicjs.com/v2/buckets/a5e294b0-55ee-11ec-942e-ef0a04148eb7/objects?pretty=true&query=%7B%22type%22%3A%22bookmarks%22%7D&read_key=fufvo5ceSy1W88afkchDIRjYrUIzErPw9YzcW2vQV1SxKqjNHo&limit=20&props=slug,title,content,metadata,"
        
        guard let url = URL(string: api) else { return }
        
        URLSession.shared.dataTask(with: url) { (data, response, error) in
            do {
                if let data = data {
                    let result = try JSONDecoder().decode(ObjectResponse.self, from: data)
                    DispatchQueue.main.async {
                        self.items = [result]
                        
                        /* Prints out everything nicely
                        result.objects.forEach {
                             print($0.title)
                        }
                        */
                        
                    }
                } else {
                    print("No data")
                }
            } catch (let error) {
                print(error)
            }
        }.resume()
    }
}

Content View

struct ContentView: View {
    @ObservedObject var viewModel = ContentViewModel()
    
    var body: some View {
        NavigationView {
            List(viewModel.items, id: \.self) { item in
                VStack(alignment: .leading) {
                    Text(item.objects[0].title) // Requires me to provide array position
                }
            }
            .navigationTitle("Bookmarks")
        }
        .onAppear(perform: {
            viewModel.fetchData()
        })
    }
}

Example JSON response

{
  "objects": [
    {
      "slug": "the-navigation-bar-isnt-hidden-as-expected-in-swiftui",
      "title": "The Navigation Bar Isn’t Hidden as Expected in SwiftUI",
      "content": null,
      "metadata": {
        "published": "28 Nov 2021 at 16:30",
        "url": "https://betterprogramming.pub/swiftui-navigationbar-is-not-really-hidden-as-you-expect-785ff0425c86",
        "snippet": "",
        "read": true
      }
    },
    {
      "slug": "hey-facebook-i-made-a-metaverse-27-years-ago",
      "title": "Hey, Facebook, I Made a Metaverse 27 Years Ago",
      "content": null,
      "metadata": {
        "published": "28 Nov 2021 at 21:39",
        "url": "https://www.theatlantic.com/technology/archive/2021/10/facebook-metaverse-was-always-terrible/620546/",
        "snippet": "Michel Baret / Gamma-Rapho / Getty In a booth at Ted’s Fish Fry, in Troy, New York, my friend Daniel Beck and I sketched out our plans for the metaverse. ",
        "read": true
      }
    },
    {
      "slug": "when-big-tech-buys-small-tech-benedict-evans",
      "title": "When big tech buys small tech — Benedict Evans",
      "content": null,
      "metadata": {
        "published": "28 Nov 2021 at 21:39",
        "url": "https://www.ben-evans.com/benedictevans/2021/11/12/when-big-tech-buys-small-tech",
        "snippet": "Acquisitions are a big part of the discussion around competition in tech today, and a big set of questions. It’s easy to say that we wouldn’t let Amazon buy Zappos or Google buy DoubleClick again, but everyone’s favourite puzzle is Instagram. ",
        "read": false
      }
    }
  ],
  "total": 171,
  "limit": 3
}
kejk
  • 1
  • 2
  • What is the desired behavior here? To loop over each item and include it in the `VStack`? Have you tried `ForEach`? – jnpdx Dec 05 '21 at 17:55
  • Hey! Yeah exactly, I want to iterate over the array to reference (for example) `Text(item.objects.title)` without having to specify which item in the array I'm looking for. Using a `ForEach` doesn't work sadly because the `List(viewModel.items, id: \.self) { item in }` is essentially replicating the `ForEach` looping. So I think it's something to do with my model but I'm not sure! – kejk Dec 05 '21 at 19:56
  • There isn’t anything preventing you from using a ForEach inside a List. I’m just unclear on what the desired output is — somehow you are going to have to deal with your nested arrays in the output. – jnpdx Dec 05 '21 at 20:24
  • Oh absolutely, I guess my challenge is I can't make sense of how to break down things further inside a `ForEach` loop to solve this. I'm guessing what you mean is that I could use a `ForEach` to iterate over the `List`'s array to break into it one layer deeper? – kejk Dec 05 '21 at 20:28
  • Got it! Yeeeesh, finally. Thanks for the push here jnpdx. – kejk Dec 05 '21 at 20:38

1 Answers1

0

Alright, solved, thanks to a little help from jnpdx. The array needed to be further drilled into via a ForEach loop, like so:

List(viewModel.items, id: \.self) { item in
 ForEach(item.objects, id: \.self) { bookmark in
   VStack(alignment: .leading) {
    Text(bookmark.title)
   }
 }
}
kejk
  • 1
  • 2