Cant parse JSON (nested dictionary) into ForEach with SwiftUI - ios

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

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

How do I filter my data displayed in my view?

My XCODE Swift VIEW code below displays a list of all Flavor Groups and Descriptors from my data. What I would like to do is filter the data to display all Flavor Groups and Descriptors except where isSeltzer is false.
I have tried using something like this in my View Model and then using iterating from the filtered array in my view but I can't get this to work:
let flavorsNoSeltzers = flavors.filter({ return $0.isSeltzer != false })
Here is example of my local JSON data:
[
{
"id": "562811",
"flavorGroup": "APRICOT",
"name": "NATURAL AND ARTIFICIAL APRICOT FLAVOR",
"isBeer": true,
"isSeltzer": false,
"isArtificial": true,
"descriptors": ["FRUITY"],
"keywords": ["juicy", "skunky", "peach", "floral", "slight green (sierra nevada pale ale)"]
},
{
"id": "U39252",
"flavorGroup": "BANANA",
"name": "NATURAL BANANA FLAVORING",
"isBeer": true,
"isSeltzer": true,
"isArtificial": false,
"descriptors": [""],
"keywords": ["missing"]
},
{
"id": "681686",
"flavorGroup": "WHITE CHOCOLATE",
"name": "NATURAL WHITE CHOCOLATE FLAVOR WONF",
"isBeer": true,
"isSeltzer": true,
"isArtificial": false,
"descriptors": ["LACTONIC", "COCOA", "CREAMY"],
"keywords": ["nutty", "milk chocolate", "french vanilla", "custard", "cakey"]
}
]
Here is an example of my MODEL:
struct Flavor: Codable, Identifiable {
enum CodingKeys: CodingKey {
case id
case flavorGroup
case name
case isBeer
case isSeltzer
case isArtificial
case descriptors
case keywords
}
let id, flavorGroup, name: String
let isBeer, isSeltzer, isArtificial: Bool
let descriptors, keywords: [String]
}
Here is an example of my VIEW MODEL:
class ReadData: ObservableObject {
#Published var flavors = [Flavor]()
init(){
loadData()
}
func loadData() {
guard let url = Bundle.main.url(forResource: "flavors", withExtension: "json")
else {
print("Json file not found")
return
}
let data = try? Data(contentsOf: url)
let flavors = try? JSONDecoder().decode([Flavor].self, from: data!)
self.flavors = flavors!
}
}
Here is an example of my VIEW:
struct myView: View {
#ObservedObject var flavorData = ReadData()
var body: some View{
List(flavorData.flavors){ flavor in
VStack(alignment: .leading) {
Text(flavor.flavorGroup)
ForEach(flavor.descriptors, id: \.self) { descriptor in
if descriptor.isEmpty {
// do nothing
} else {
Text("- \(descriptor)")
}
}
}
}
}
}
A simplified working example that filters the list of Flavor objects and returns a new list with only Flavor objects where isSeltzer is true.
import Foundation
struct Flavor {
let id: String
let isSeltzer: Bool
}
let flavors = [Flavor(id: "First", isSeltzer: false), Flavor(id: "Second", isSeltzer: true), Flavor(id: "Third", isSeltzer: false)]
let flavorsSeltzersTrue = flavors.filter({ $0.isSeltzer })
print(flavorsSeltzersTrue) // Prints the flavors with isSeltzer == true
If you want the opposite (i.e. where isSeltzer is false), you can simply change $0.isSeltzer in the closure to !$0.isSeltzer. Also, check out the documentation on the filter(_:) method.
If this doesn't fix your issue, then there's something else going on with your code.
EDIT:
In your code, you might want to add this line of code like this:
class ReadData: ObservableObject {
#Published var flavors = [Flavor]()
init(){
loadData()
}
func loadData() {
guard let url = Bundle.main.url(forResource: "flavors", withExtension: "json")
else {
print("Json file not found")
return
}
let data = try? Data(contentsOf: url)
let flavors = try? JSONDecoder().decode([Flavor].self, from: data!)
self.flavors = flavors.filter({ $0.isSeltzer }) // I've added it here.
}
}
Please note that force unwrapping (flavors!) is bad practice and might lead to crashes when flavors is nil.

How do I create a search for the items in an section in SwiftUI?

I need to create a search logic for NameString instead of NameSection. I was able to make the search bar here (How do I create a search bar for SwiftUI?). I want to search names instead of sections. I tried numerous times but it won't work. NameString thinks it's not String.
So in the JSON File there are sections like Monday, Wednesday, etc, currently the code supports a search for those instead of items in those days like Name11, Name 12. I need to create a search for those items.
Here is what I have
import SwiftUI
var counter = 0
struct ContentView: View {
let name = Bundle.main.decode([NameSection].self, from: "name copy.json")
let names = Bundle.main.decode([NameString].self,from: "name copy.json")
#State private var searchText = ""
var body: some View {
NavigationView{
VStack {
SearchBarView(searchText: $searchText)
List{
ForEach(name.filter{$0.name.hasPrefix(searchText) || searchText == ""}, id:\.self) {
section in
Section(header: Text(section.name)) {
ForEach(section.items){ item in
NavigationLink(destination:
TrackerDetails(item: item)){
Tracker(item: item)
}
}
}
}
}
.navigationTitle("Names")
.listStyle(GroupedListStyle())
}
}
}
}
import SwiftUI
struct NameSection: Codable, Identifiable, Hashable {
var id: UUID
var name: String
var items: [NameItem]
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
struct NameItem: Codable, Equatable, Identifiable {
var id: UUID
var name: String
var mainImage: String {
name.replacingOccurrences(of: " ", with: "-").lowercased()
}
var thumbnailImage: String {
"\(mainImage)-thumb"
}
#if DEBUG
static let example = NameItem(id: UUID(), name: "Maple French Toast")
#endif
}
struct NameString: Codable, Identifiable{
var id: UUID
var name: String
}
JSON
[{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A66",
"name": "Monday",
"items": [{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A67",
"name": "Name11"
},
{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A68",
"name": "Name12"
},
{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A69",
"name": "Name13"
}
]
},
{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A67",
"name": "Wednesday",
"items": [{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A60",
"name": "Name21"
},
{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A61",
"name": "Name22"
},
{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A62",
"name": "Name23"
}
]
},
{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A90",
"name": "SATURDAY",
"items": [{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A96",
"name": "Name21"
},
{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A94",
"name": "Name22"
},
{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A35",
"name": "Name23"
}
]
},
]
JSON Decoder
import UIKit
extension Bundle {
func decode<T: Decodable>(_ type: T.Type, from file: String) -> T {
guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Failed to locate \(file) in bundle.")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("Failed to load \(file) from bundle.")
}
let decoder = JSONDecoder()
guard let loaded = try? decoder.decode(T.self, from: data) else {
fatalError("Failed to decode \(file) from bundle.")
}
return loaded
}
}
At your request in a comment on yesterdays question, here's a brief function to filter the results based on the name of the item:
struct ContentView: View {
let name = Bundle.main.decode([NameSection].self, from: "name.json")
#State private var searchText = ""
var filteredItems : [NameSection] {
if searchText.isEmpty { return name }
return name.map { nameSection in
var nameSectionCopy = nameSection
nameSectionCopy.items = nameSection.items.filter { $0.name.lowercased().contains(searchText.lowercased()) }
return nameSectionCopy
}.filter { !$0.items.isEmpty }
}
var body: some View {
NavigationView{
VStack {
SearchBarView(searchText: $searchText)
List{
ForEach(filteredItems, id:\.self) {
section in
Section(header: Text(section.name)) {
ForEach(section.items){ item in
Text(item.name)
}
}
}
.navigationTitle("Names")
.listStyle(GroupedListStyle())
}
}
}
}
}
It checks for a case-insensitive match (see that both searchText and name are lowercased()) that appears anywhere in the name field (see contains()).
It filters out sections (ie Days) that don't have any items (see the last filter of filteredItems).
I didn't have access to your TrackerDetails or Tracker, so you'll have to re-implement your NavigationLink.

Cannot Decode JSON Response Using Swift

I am trying to complete a pretty standard thing: decode a JSON response. I can get the data, however decoding does not succeed - worse, there is no error message. Print statement 1 & 2 output with the correct data but I get no output from print statement 3, it outputs print statement 4 instead (e.g. my error message).
I am hoping someone can see my probably obvious error, thanks for any hints that could resolve me issue!
Phil
(Edit) Added JSON Response:
[{
"open": {
"price": 122.52,
"time": 1668853732275
},
"close": {
"price": 125.44,
"time": 1658436480762
},
"high": 125.35,
"low": 123.57,
"volume": 75244144,
"symbol": "AAPL"
}]
struct ResponsePrice: Codable {
var results: [ResultPrice]
}
struct ResultPrice: Codable {
var open: ResultPriceTime
var close: ResultPriceTime
var high: Double
var low: Double
var volume: Double
var symbol: String
}
struct ResultPriceTime: Codable {
var price: Double
var time: Double
}
///
struct SymbolView: View {
#State private var results = [ResultPrice]()
var body: some View {
List(resultsPrice, id: \.symbol) { item in
VStack(alignment: .leading) {
Text(item.symbol)
.font(.headline)
Text(item.symbol)
}
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://sandbox.iexapis.com/stable/stock/market/ohlc?symbols=aapl&token=Tpk_af03c7f5bac14742a7ce77969a791c66") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
do {
if let data = data {
//let stringData = String(decoding: data, as: UTF8.self)
//print(stringData)
if let decodedResponse = try JSONDecoder().decode(ResponsePrice.self, from: data) {
// we have good data – go back to the main thread
print("Fetched: \(url)")
DispatchQueue.main.async {
// update our UI
self.results = decodedResponse.results
print("Ticker:\(self.results[0].T)")
}
// everything is good, so we can exit
return
}
}
}
catch {
fatalError("Couldn't parse as :\n\(error)")
}
// if we're still here it means there was a problem
print("4 Decode failed: \(error?.localizedDescription ?? "Unknown error")")
// step 4
}.resume()
}
}
struct SymbolView_Previews: PreviewProvider {
static var previews: some View {
SymbolView()
}
}

Swift 5 + Alamofire 5.*: Decode data with same root object at the top

My problem is that I have JSON object got from the server like this one:
{
"data": [
{
"id": 1,
"name": "at",
"amount": 446,
"createdAt": "25/04/2020",
"updatedAt": "25/04/2020"
},
{
"id": 2,
"name": "iste",
"amount": 872,
"createdAt": "25/04/2020",
"updatedAt": "25/04/2020"
}
]
}
And I have Codable struct that decodes this object:
struct Expense: Codable, Identifiable {
var id: Int
var name: String
var amount: String
var createdAt: String
var updatedAt: String
}
Also, I have class with a static method that will do the AF request, also I'm using FuturePromise library for hendling completionof the request:
struct RequestAPI {
#discardableResult
static func callAndDecode<T:Decodable>(route:APIRouter, decoder: JSONDecoder = JSONDecoder()) -> Future<T> {
return Future(operation: { completion in
AF.request(route).responseDecodable(decoder: decoder, completionHandler: { (response: DataResponse<T, AFError>) in
switch response.result {
case .success(let value):
completion(.success(value))
case .failure(let error):
print(error.localizedDescription)
completion(.failure(error))
}
})
})
}
}
Problem is that I have a root "data" parameter that sometimes is present and sometimes not.
I know that there is a solution that I can create a Result codable Model that will be the parent of the Expense Model, but that does not approach that I want, because what will happen if I will have 20 different models I'll have to create 20 deferent root Models?
Yes, I can do it with CodingKeys but that is a little bit hacky and too much of code for this simple task.
So the best approach is to add something like this:
struct ExpensesList: Codable {
var data: [Expense]
}
But for me, it is a problem that I will always have 'data' root, so then for any model I will have some 'List' model.
Is there a better approach that is not hacky or this is the only one.
Maybe to send a child model to one data model, but how to recognize it in views,...?
Thank you in advance.
If I understand you correctly, you can make your Root structure generic, and should also make the data property optional.
struct Foo: Decodable {
let foo: Int
}
struct Bar: Decodable {
let bar: String
}
struct Root<T: Decodable>: Decodable {
let data: [T]?
}
typealias FooList = Root<Foo>
typealias BarList = Root<Bar>
let fooData = """
{ "data": [ { "foo": 42 } ] }
""".data(using: .utf8)!
let barData = """
{ "data": [ { "bar": "baz" } ] }
""".data(using: .utf8)!
do {
let foos = try JSONDecoder().decode(FooList.self, from: fooData)
let bars = try JSONDecoder().decode(BarList.self, from: barData)
} catch {
print(error)
}

How to parse this JSON and create struct for

I have a JSON:
[
{
"Men": {
"image": "/url.png",
"Jeans": [
{
"name": "asd",
"about": "sadvc",
"image": "/urls.sjd"
},
{
"name": "asd",
"about": "sadvc",
"image": "/urls.sjd"
},
{
"name": "asd",
"about": "sadvc",
"image": "/urls.sjd"
}
]
},
"Women": {
"image": "/url2.jpeg",
"All": {}
}
}
]
How to create the struct for "step by step" going into the tableview?
First View - Change sex - Women or men.
Second - Change type - jeans or other...
Thirst - collection view with jeans (name, about and price).
Now, i have struct
struct Clothe: Decodable {
let about: String
let name: String
let image: String
}
And func for downloading JSON
var clothes = [Clothe]()
public func downloadJSON(completed: #escaping () -> ()) {
let url = URL(string: "https...bla-bla/ULRhere.json")
let request = URLRequest(url: url!, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 120.0)
URLSession.shared.dataTask(with: request) { (data, response, error) in
if error == nil {
do {
self.clothes = try JSONDecoder().decode([Clothe].self, from: data!)
print(self.clothes)
DispatchQueue.main.async {
completed()
}
} catch {
print("JSON Error")
}
}
}.resume()
}
let json = """{"Men": {"image": "/url.png","Jeans": [{"name": "asd","about": "sadvc","image": "/urls.sjd"},{"name": "asd","about": "sadvc","image": "/urls.sjd"},{"name": "asd","about": "sadvc","image": "/urls.sjd"}]},"Women": {"image": "/url2.jpeg","All": {}}}""".data(using: .utf8)!
struct Cloth: Decodable {
let Men : MenStruct?
let Women : WomanStruct?}
struct MenStruct: Decodable {
let image: String?
let Jeans: [JeansStruct]?}
struct JeansStruct: Decodable {
let name: String?
let about: String?
let image: String?}
struct WomanStruct: Decodable {
let image: String?}
func executeJson(){
do {
let cloth = try JSONDecoder().decode(Cloth.self, from: json)
print(cloth)
}catch {
print("JSON Error")
}}
executeJson()
Cloth(Men: Optional(__lldb_expr_88.MenStruct(image: Optional("/url.png"), Jeans: Optional([__lldb_expr_88.JeansStruct(name: Optional("asd"), about: Optional("sadvc"), image: Optional("/urls.sjd")), __lldb_expr_88.JeansStruct(name: Optional("asd"), about: Optional("sadvc"), image: Optional("/urls.sjd")), __lldb_expr_88.JeansStruct(name: Optional("asd"), about: Optional("sadvc"), image: Optional("/urls.sjd"))]))), Women: Optional(__lldb_expr_88.WomanStruct(image: Optional("/url2.jpeg"))))
This is not a good JSON design. I would suggest not using data values ("Men" and "Women" or "Jeans" vs another type of clothing) as keys to your dictionary.
Also, I'd suggest that the response from your web service return a dictionary with keys like a success value (a Boolean that indicates whether the result was successful or not) and a result key (for the contents of the response). This way, if there is an error, the basic structure of the response will be the same (but a successful response will include result key and a failure might include an error message or error code).
Anyway, I'd suggest something like:
{
"success": true,
"result": [{
"name": "Men",
"image": "men.png",
"productLine": [{
"name": "Jeans",
"image": "jeans.png",
"products": [{
"name": "Slim fit",
"about": "Slim Fit Jeans",
"image": "slim.png"
},
{
"name": "Bell Bottom",
"about": "Cool bell bottom jeans",
"image": "bellbottom.png"
},
{
"name": "Acid Wash",
"about": "Acid wash jeans",
"image": "acid.png"
}
]
}]
}, {
"name": "Women",
"image": "women.jpeg"
}]
}
Then you can set up logical model entities:
struct Product: Codable {
let about: String
let name: String
let image: String
}
struct ProductLine: Codable {
let name: String
let image: String
let products: [Product]?
}
struct CustomerCategory: Codable {
let name: String
let image: String
let productLine: [ProductLine]?
}
Then you'd process the response like so:
func processResponse(_ data: Data) {
struct ResponseObject: Codable {
let success: Bool
let errorCode: Int?
let result: [CustomerCategory]?
}
do {
let responseObject = try JSONDecoder().decode(ResponseObject.self, from: data)
guard responseObject.success, let customerCategories = responseObject.result else {
// handle server error here
return
}
print(customerCategories)
} catch {
print(error)
}
}
This allows you to add new customer categories (e.g. children) or product lines (e.g. things other than jeans) without affecting the basic interface with the server.
In your other question, you've changed the nature of the data being returned, but, again, I'd advise against putting data attributes in the keys of your dictionaries.

Resources