Home > database >  Cant parse JSON (nested dictionary) into ForEach with SwiftUI
Cant parse JSON (nested dictionary) into ForEach with SwiftUI

Time:05-08

I want to parse the following JSON into a list with SwiftUI, however I am experiencing some issues with this.

JSON (there are many more entires than this with random Identifiers as the entry. This is what caused it to be much harder to perform):

{
  "DCqkboGRytms": {
    "name": "graffiti-brush-3.zip",
    "downloads": "5",
    "views": "9",
    "sha256": "767427c70401d43da30bc6d148412c31a13babacda27caf4ceca490813ccd42e"
  },
  "gIisYkxYSzO3": {
    "name": "twitch.png",
    "downloads": "19",
    "views": "173",
    "sha256": "aa2a86e7f8f5056428d810feb7b6de107adefb4bf1d71a4ce28897792582b75f"
  },
  "bcfa82": {
    "name": "PPSSPP.ipa",
    "downloads": "7",
    "views": "14",
    "sha256": "f8b752adf21be6c79dae5b80f5b6176f383b130438e81b49c54af8926bce47fe"
  }
}

SwiftUI Codables:

struct Feed: Codable {
    let list: [FeedValue]
}

struct FeedValue: Codable, Identifiable {
    var id = UUID()
    let name, downloads, views, sha256: String
}

JSON Fetch method (and parse):

func getFeeds(completion:@escaping (Feed) -> ()) {

    // URL hidden to respect API privacy
    
    guard let url = URL(string: "") else { return }
    URLSession.shared.dataTask(with: url) { (data, _, _) in
        
        let feed = try! JSONDecoder().decode(Feed.self, from: data!)
        
        DispatchQueue.main.async {
            completion(feed)
        }
        
    }.resume()
    
}

Finally, the view containing my ForEach loop:

struct HomeView: View {
    
    @State private var scrollViewContentOffset: CGFloat = .zero
    
    @State var listFeed = Feed(list: [])
    
    var body: some View {
        
        // Z-coord status bar holder
        ZStack(alignment: .top) {
            TrackableScrollView(.vertical, showIndicators: true, contentOffset: $scrollViewContentOffset) {
                
                
                // Public files Start
                LazyVStack(spacing: 0) { {
                    // Withholds our content for each asset retrieved.

                    // Main part
                    ForEach(listFeed.list, id: \.id) { download in
                        AssetView(name: .constant(download.name), downloads: .constant(download.downloads), views: .constant(download.views))
                    }

                }.padding([.leading, .top, .trailing], 25)
                // Public files End
                
            }.edgesIgnoringSafeArea([.leading, .trailing, .top])
            
        }.background(Color.backgroundColor.ignoresSafeArea())
            .onAppear() {
                getFeeds { feed in
                    //print(feed) : this did happen to work at the time of testing
                    self.listFeed = feed
                }
            }
        
    }
}

And for the error that is being presented:

Starfiles/HomeView.swift:32: Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "list", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "list", intValue: nil) ("list").", underlyingError: nil))

I've spent several hours today changing the method of parsing, but was unable to find a solution on my own.

EDIT 1: I would like to mention that I need a way to access the dictionary ID's as well, as they'll be used for entering in a new view.

EDIT 2: Using what @jnpdx provided, JSON parsing now works as expectedly, and gets the information I need. However there's an issue with ForEach loops, the same one which persisted before (It does work in a List(), as his answer showed).

Here is the updated code (not going to show any code that's irrelevant)

                    ForEach(listItems) { item in // Cannot convert value of type '[IdentifiableFeedValue]' to expected argument type 'Binding<C>' &  Generic parameter 'C' could not be inferred
                        AssetView(name: .constant(item.feedValue.name), // Cannot convert value of type 'Binding<Subject>' to expected argument type 'String'
    downloads: .constant(item.feedValue.downloads), views: .constant(item.feedValue.views))
                    }.onAppear() {
                        // URL hidden to respect API privacy, refer to JSON above.
                        guard let url = URL(string: "") else { return }
                        URLSession.shared.dataTask(with: url) { (data, _, _) in
                            
                            do {
                                list = try JSONDecoder().decode([String:FeedValue].self, from: data!)
                            } catch {
                                print("error")
                            }
                            
                        }.resume()
                    }

// AssetView
struct AssetView: View {
    
    @Binding var name: String
    @Binding var downloads: String
    @Binding var views: String

...

}

CodePudding user response:

Your JSON doesn't have any keys called list, which is why it's failing. Because it's a dynamically-keyed dictionary, you will want to decode it as [String:FeedValue].

I used a wrapper to keep the id from the original JSON, but you could also do some fancier decoding if you wanted to keep it in a one-level struct, but that's beyond the scope of this question.

let jsonData = """
{
  "DCqkboGRytms": {
    "name": "graffiti-brush-3.zip",
    "downloads": "5",
    "views": "9",
    "sha256": "767427c70401d43da30bc6d148412c31a13babacda27caf4ceca490813ccd42e"
  },
  "gIisYkxYSzO3": {
    "name": "twitch.png",
    "downloads": "19",
    "views": "173",
    "sha256": "aa2a86e7f8f5056428d810feb7b6de107adefb4bf1d71a4ce28897792582b75f"
  },
  "bcfa82": {
    "name": "PPSSPP.ipa",
    "downloads": "7",
    "views": "14",
    "sha256": "f8b752adf21be6c79dae5b80f5b6176f383b130438e81b49c54af8926bce47fe"
  }
}
""".data(using: .utf8)!

struct FeedValue: Codable {
    let name, downloads, views, sha256: String
}

struct IdentifiableFeedValue : Identifiable {
    let id: String
    let feedValue: FeedValue
}

struct ContentView: View {
    @State var list : [String:FeedValue] = [:]
    
    var listItems: [IdentifiableFeedValue] {
        list.map { IdentifiableFeedValue(id: $0.key, feedValue: $0.value) }
    }
    
    var body: some View {
        List(listItems) { item in
            Text(item.feedValue.name)
        }.onAppear {
            do {
                list = try JSONDecoder().decode([String:FeedValue].self, from: jsonData)
            } catch {
                print(error)
            }
        }
    }
}
  • Related