SwiftUI - Display nested Array in JSON response - try breaking up the expression into distinct sub-expressions - json

I have the following JSON response.
{
"results":[
{
"TrimName":"Truck",
"VIN":"GTJ3E18JF164623",
"Price": 47600,
"Odometer": 35511,
"OdometerType": "Miles",
"OptionCodeSpecs":{
"C_SPECS":{
"code":"C_SPECS",
"name":"Specs",
"options":[
{
"code":"SPECS_ACCELERATION",
"name":"5.2 sec",
"long_name":"5.2 seconds",
"description":"0-60 mph"
},
{
"code":"SPECS_TOP_SPEED",
"name":"140 mph",
"long_name":"140 miles per hour",
"description":"Top Speed"
},
{
"code":"SPECS_RANGE",
"name":"264 mi",
"long_name":"264 miles",
"description":"range (EPA)"
}
]
}
}
}
],
"total_matches_found":"53"
}
I built the Struct using app.quicktype.io.
// MARK: - InventoryModel
struct InventoryModel: Codable {
let results: [Result]
let totalMatchesFound: String
enum CodingKeys: String, CodingKey {
case results
case totalMatchesFound = "total_matches_found"
}
}
// MARK: - Result
struct Result: Codable {
let trimName, vin: String
let price, odometer: Int
let odometerType: String
let optionCodeSpecs: OptionCodeSpecs
enum CodingKeys: String, CodingKey {
case trimName = "TrimName"
case vin = "VIN"
case price = "Price"
case odometer = "Odometer"
case odometerType = "OdometerType"
case optionCodeSpecs = "OptionCodeSpecs"
}
}
// MARK: - OptionCodeSpecs
struct OptionCodeSpecs: Codable {
let cSpecs: CSpecs
enum CodingKeys: String, CodingKey {
case cSpecs = "C_SPECS"
}
}
// MARK: - CSpecs
struct CSpecs: Codable {
let code, name: String
let options: [Option]
}
// MARK: - Option
struct Option: Codable {
let code, name, longName, optionDescription: String
enum CodingKeys: String, CodingKey {
case code, name
case longName = "long_name"
case optionDescription = "description"
}
}
I am able to successfully retrieve the JSON response successfully.
#Published var getInventoryQuery: InventoryModel = InventoryModel(results: [], totalMatchesFound: "")
In my ContentView I am able to display the data at the Results object, but I am unable to get to the options array in the OptionCodeSpecs object without the following error:
The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
Xcode Version 12.5.1 (12E507)
#ObservedObject var inv = GetInventory()
The following works great. Code tripped out for clarity.
ScrollView(.horizontal, showsIndicators: false){
HStack(spacing: 10){
ForEach(inv.getInventoryQuery.results, id: \.trimName) { inventory in
VStack(alignment: .leading) {
VStack(alignment: .center) {
Text(inventory.trimName)
However, when I try and get to the options array, I get the following error:
The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
HStack(alignment: .center){
ForEach(inv.getInventoryQuery.results[inventory].optionCodeSpecs.cSpecs.options) { options in
Text(options.longName)
.font(.headline)
.fontWeight(.light)
Image(systemName: "battery.100")
.renderingMode(.original)
}
}

Related

Swift JSON object with name of something/Integer

I'm using TMDB api and fetching tv show seasons, but seasons I get back are not inside array, but as objects with names: season/1, season/2. I need to be able to parse tv show with any number of seasons
Is there a way I can convert this to array without worring about how many seasons does the show have?
struct Result: Codable {
var season1: Season?
var season2: Season?
var id: Int?
enum CodingKeys: String, CodingKey {
case season1
case season2
case id
}
}
struct Season: Codable {
var id: Int?
enum CodingKeys: String, CodingKey {
case id
}
}
{
"id" : 1234,
"season/1": {
"id": 1234
},
"season/2": {
"id": 12345
}
}
EDIT:
Found a solution in dynamic coding keys
private struct DynamicCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
return nil
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
var tempArray = [TestSeason]()
for key in container.allKeys {
if key.stringValue.contains("season/") {
let decodedObject = try container.decode(TestSeason.self, forKey: DynamicCodingKeys(stringValue: key.stringValue)!)
tempArray.append(decodedObject)
} else {
print("does not contain season \(key.stringValue)")
}
}
season = tempArray
}
You're getting back a Dictionary, which you can access directly without using your Results struct. The dictionary probably provides a more flexible way of accessing the data than an array, but can also easily be converted to an Array.
As you haven't stated how you'd like the output, the below will convert them to an array of tuples, where each tuple is (season, id)
let data = json.data(using: .utf8)!
let decoder = JSONDecoder()
do {
let results = try decoder.decode([String:Season].self, from: data)
.map{($0.key, $0.value.id )}
print(results) // [("season/2", 12345), ("season/1", 1234)]
} catch {
print(error)
}
Edit: You also don't need the CodingKeys in Season as it can be inferred from the properties. All you need is
struct Season: Codable {
let id: Int
}

parsing nested JSON in Swift 5

Can't figure out how to build the struct for this nested JSON. I'm so close but missing something..
I'm trying to verify I'm loading correctly... the first two work great the nested data fails
print(json.pagination.items) // this works
print(json.releases[3].date_added) // this works
print(json.releases[3].basicInformation?.year) //NOT WORKING, returns nil
here is the struct in built
struct Response: Codable {
let pagination: MyResult
let releases: [MyReleases]
}
struct MyResult: Codable {
var page: Int
var per_page: Int
var items: Int
}
struct MyReleases: Codable {
var date_added: String
let basicInformation: BasicInformation?
}
struct BasicInformation: Codable {
let title: String
let year: Int
enum CodingKeys: String, CodingKey {
case title, year
}
}
My JSON is
{
"pagination":{
"page":1,
"pages":2,
"per_page":50,
"items":81,
"urls":{
"last":"https://api.discogs.com/users/douglasbrown/collection/folders/0/releases?page=2&per_page=50",
"next":"https://api.discogs.com/users/douglasbrown/collection/folders/0/releases?page=2&per_page=50"
}
},
"releases":[
{
"id":9393649,
"instance_id":656332897,
"date_added":"2021-03-28T10:54:09-07:00",
"rating":2,
"basic_information":{
"id":9393649,
"master_id":353625,
"master_url":"https://api.discogs.com/masters/353625",
"resource_url":"https://api.discogs.com/releases/9393649",
"thumb":"",
"cover_image":"",
"title":"Ten Summoner's Tales",
"year":2016
}
}
]
}
Any help would be greatly appreciated. I'm so close but missing something... :(
First of all
json.releases[3].date_added
doesn't work with the given JSON, it will crash because there is only one release.
In MyReleases you have to add CodingKeys to map the snake_case name(s)
struct MyReleases: Codable {
var dateAdded: String
let basicInformation: BasicInformation?
private enum CodingKeys: String, CodingKey {
case basicInformation = "basic_information", dateAdded = "date_added"
}
}
or add the .convertFromSnakeCase key decoding strategy.
You can even decode dateAdded as Date with the .iso8601 date decoding strategy.

SwiftUI ForEach loop multiple arrays

I'm having a difficult time trying to figure out how to ForEach loop some array's of array's of json data in SwiftUI. I used https://app.quicktype.io to get my data struct from this URL here.
I'm looking to get classes FeaturedHeaderView and FeaturedPackageView, which have title's of "Hot Right Now" and "What We're Using" which also contain the FeaturedPackageView data. My problem is I'm only looping through the first FeaturedHeaderView and FeaturedPackageView repeatedly, which I assumed there was two for each section. Is my data struct incorrect? I've never attempted complex json data yet, so I'm unsure how to handle it properly and if the ForEach loop is what I'm looking for. The end goal would be to have a List with `"Hot Right Now" and it's items and then then "What We're Using" and it's items.
I was able to get the FeaturedBannersView class just fine using two ForEach loops and thought it would be the same approach for the rest of the data?
The banner view working
ScrollView(.horizontal) {
HStack {
ForEach(self.welcome.views, id: \.viewClass) { views in
ForEach(views.banners ?? [], id:\.url) { banner in
ZStack (alignment: .bottomLeading) {
GeometryReader { geometry in
RequestImage(Url(banner.url), animation: nil)
.aspectRatio(contentMode: .fit)
.frame(width: geometry.size.width)
.clipped()
.cornerRadius(CGFloat(views.itemCornerRadius ?? 0))
}
HStack {
Text(banner.title ?? "")
.fontWeight(.bold)
.font(.title3)
.foregroundColor(Color.white)
}
.padding(.all, 15)
}
.frame(width: 263, height: 148)
}
}
}
.padding(.leading, 10)
.padding(.trailing, 10)
}
The issue
My data struct
struct Welcome: Codable {
let views: [WelcomeView]
}
struct WelcomeView: Codable {
let viewClass: String?
let banners: [Banner]?
let views: [PurpleView]?
enum CodingKeys: String, CodingKey {
case viewClass = "class"
case views
}
}
struct Banner: Codable {
let url: String?
let title, package, repoName: String?
}
struct PurpleView: Codable {
let viewClass: String?
let views: [FluffyView]
enum CodingKeys: String, CodingKey {
case viewClass = "class"
case views
}
}
struct FluffyView: Codable {
let viewClass: String?
let title: String?
let package, packageName, packageAuthor: String?
let repoName: RepoName?
let packageIcon: String?
enum CodingKeys: String, CodingKey {
case viewClass = "class"
case title, package, packageName, packageAuthor, repoName, packageIcon
}
}
enum RepoName: String, Codable {
case chariz = "Chariz"
case packix = "Packix"
}
List {
ForEach(self.welcome.views, id: \.viewClass) { view in
ForEach(view.views ?? [], id: \.viewClass) { purple in
ForEach(purple.views, id: \.packageName) { fluffy in
if fluffy.viewClass == "FeaturedHeaderView" {
Text(fluffy.title ?? "")
.font(.title3)
.fontWeight(.bold)
} else {
HStack (spacing: 15){
RequestImage(Url(fluffy.packageIcon ?? ""), animation: nil)
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.clipped()
.cornerRadius(13.5)
VStack (alignment: .leading){
Text(fluffy.packageName ?? "")
.font(.body)
Text(fluffy.packageAuthor ?? "")
.foregroundColor(Color.secondary)
.font(.callout)
Text(fluffy.repoName?.rawValue ?? "")
.foregroundColor(Color.secondary)
.font(.subheadline)
}
}
}
}
}
}
}
.onAppear() {
request()
}
The reason that you're getting the repeats is that you're using .viewClass and .packageName as your ids for the ForEach loops, but in the JSON, those values are not actually unique. FeaturedStackView, for example, gets repeated.
How to solve it:
Add IDs to your models and use them for the ForEach keys.
// MARK: - WelcomeView
struct WelcomeView: Codable {
var id = UUID() //<-- HERE
let viewClass: String
let itemSize: String?
let itemCornerRadius: Int?
let banners: [Banner]?
let horizontalSpacing: Int?
let views: [PurpleView]?
enum CodingKeys: String, CodingKey {
case viewClass = "class"
case itemSize, itemCornerRadius, banners, horizontalSpacing, views
}
}
struct PurpleView: Codable {
var id = UUID()
let viewClass: String
let preferredWidth: Int
let views: [FluffyView]
let xPadding: Int?
enum CodingKeys: String, CodingKey {
case viewClass = "class"
case preferredWidth, views, xPadding
}
}
struct FluffyView: Codable {
var id = UUID()
let viewClass: String
let title: String?
let useBoldText: Bool?
let package, packageName, packageAuthor: String?
let repoName: RepoName?
let packageIcon: String?
let orientation: String?
let views: [TentacledView]?
let text: String?
let action: String?
let yPadding: Int?
enum CodingKeys: String, CodingKey {
case viewClass = "class"
case title, useBoldText, package, packageName, packageAuthor, repoName, packageIcon, orientation, views, text, action, yPadding
}
}
Then, in each of your ForEach loops, use id: \.id

Swift Codable Dictionary

I'm having an issue getting codable going. Any help would greatly appreciated. I have the following in my playground
Sample from my JSON file. It has many more elements, reduced it a smaller subset.
{
"metadata" : {
"generated" : {
"timestamp" : 1549331723,
"date" : "2019-02-04 20:55:23"
}
},
"data" : {
"CA" : {
"country-id" : 25000,
"country-iso" : "CA",
"country-eng" : "Canada",
"country-fra" : "Canada",
"date-published" : {
"timestamp" : 1544561785,
"date" : "2018-12-11 15:56:25",
"asp" : "2018-12-11T15:56:25.4141468-05:00"
}
},
"BM" : {
"country-id" : 31000,
"country-iso" : "BM",
"country-eng" : "Bermuda",
"country-fra" : "Bermudes",
"date-published" : {
"timestamp" : 1547226095,
"date" : "2019-01-11 12:01:35",
"asp" : "2019-01-11T12:01:35.4748399-05:00"
}
}
}
}
From The quicktype app. It generated a dictionary for Datum. The way the json is structured, the country abbreviation doesn't have a tag.
import Foundation
// MARK: - Welcome
struct Welcome: Codable {
let metadata: Metadata?
let data: [String: Datum]?
}
// MARK: - Datum
struct Datum: Codable {
let countryID: Int?
let countryISO, countryEng, countryFra: String?
let datePublished: DatePublished?
enum CodingKeys: String, CodingKey {
case countryID = "country-id"
case countryISO = "country-iso"
case countryEng = "country-eng"
case countryFra = "country-fra"
case datePublished = "date-published"
}
}
// MARK: - DatePublished
struct DatePublished: Codable {
var timestamp: Int
var date, asp: String
}
// MARK: - Metadata
struct Metadata: Codable {
var generated: Generated
}
// MARK: - Generated
struct Generated: Codable {
var timestamp: Int
var date: String
}
// MARK: - Encode/decode helpers
class JSONNull: Codable, Hashable {
public static func == (lhs: JSONNull, rhs: JSONNull) -> Bool {
return true
}
public var hashValue: Int {
return 0
}
public func hash(into hasher: inout Hasher) {
// No-op
}
public init() {}
public required init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if !container.decodeNil() {
throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull"))
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encodeNil()
}
}
From my code, I can load the json file, I'm not sure how to process the data here with the dictionary, and the country not having a name for the country abbreviation.
guard let url = Bundle.main.url(forResource: "data", withExtension: "json") else { return 0 }
let jsonData = try Data(contentsOf: url)
Note: This is a follow up to my earlier question: Swift Codable Parsing keyNotFound
Your data models are already defined correctly (however, I'd suggest some name changes and removing mutability/optionality from the properties).
Once you've parsed the JSON, there's no need to keep the Dictionary, since the keys are actually part of the value under the country-iso key.
So once you decoded your Root object, I would suggest simply keeping root.data.values, which gives you Array<CountryData>, which you can handle easily afterwards.
struct Root: Codable {
let data: [String: CountryData]
}
struct CountryData: Codable {
let countryID: Int
let countryISO, countryEng, countryFra: String
let datePublished: DatePublished
enum CodingKeys: String, CodingKey {
case countryID = "country-id"
case countryISO = "country-iso"
case countryEng = "country-eng"
case countryFra = "country-fra"
case datePublished = "date-published"
}
}
// MARK: - DatePublished
struct DatePublished: Codable {
let timestamp: Int
let date, asp: String
}
do {
let root = try JSONDecoder().decode(Root.self, from: countryJson.data(using: .utf8)!)
let countries = root.data.values
print(countries)
} catch {
error
}

How to make structs for this JSON with Decodable protocol?

I've got this JSON:
{
"$type": "DTOMapper.DTOResponseList`1[[Telemed.Dto.DTOTip, Telemed.Dto]], DTOMapper",
"ResponseList": {
"$type": "System.Collections.Generic.List`1[[Telemed.Dto.DTOTip, Telemed.Dto]], mscorlib",
"$values": [
{
"$type": "Telemed.Dto.DTOTip, Telemed.Dto",
"Title": "NO TE JUNTES CON LUQUITAS",
"Text": "Porque si tenes un amigo lucas y otro amigo lucas, tenés dos lucas. Pero no te sirven para pagar nada",
"GroupName": "TGC.Tips1",
"ConfigurationPath": "TelemedGlobalConfig>Tips>Tips[0]"
},
{
"$type": "Telemed.Dto.DTOTip, Telemed.Dto",
"Title": "no te emborraches en las fiestas",
"Text": "Terminarás pateando globos",
"GroupName": "TGC.Tips2",
"ConfigurationPath": "TelemedGlobalConfig>Tips>Tips[1]"
}
]
},
"StatusCode": 200,
"ErrorId": 0
}
And I'm trying to get access to Title and Text from the array $values.
Here are my current structs but Root gives me errors.
struct Root : Decodable { // <<< Type 'Root' does not conform to protocol 'Decodable'
private enum CodingKeys : String, CodingKey { case responseList = "ResponseList" }
let responseList : ResponseList // <<< Use of undeclared type 'ResponseList'
}
struct Values : Decodable {
private enum CodingKeys : String, CodingKey {
case title = "Title"
case text = "Text"
}
let title : String
let text : String
}
What is the correct way to make this? Also, do I have to make a struct and let for everything? Even for things I won't use, like $type, GroupName?
What is the correct way to make this?
do {
let res = try JSONDecoder().decode(Root.self, from: data)
}
catch {
print(error)
}
struct Root: Codable {
let type: String
let responseList: ResponseList
let statusCode, errorID: Int
enum CodingKeys: String, CodingKey {
case type = "$type"
case responseList = "ResponseList"
case statusCode = "StatusCode"
case errorID = "ErrorId"
}
}
// MARK: - ResponseList
struct ResponseList: Codable {
let type: String
let values: [Value]
enum CodingKeys: String, CodingKey {
case type = "$type"
case values = "$values"
}
}
// MARK: - Value
struct Value: Codable {
let title, text:String // left only <<< access to Title and Text
enum CodingKeys: String, CodingKey {
case title = "Title"
case text = "Text"
}
}
Do I have to make a struct and let for everything? Even for things I won't use, like $type, GroupName?
No only properties that you'll use
You could try this:
struct YourStructName: Codable {
var statusCode: Int
var errorId: Int
var type: String // Maybe make this an enum case
var response: Response
enum CodingKeys: String, CodingKey {
case statusCode = "StatusCode"
case errorId = "ErrorId"
case type = "$type"
case response = "ResponseList"
}
struct Response: Codable {
var type: String // Again, consider making this an enum case
var values: [ResponseValue]
enum CodingKeys: String, CodingKey {
case type = "$type"
case values = "$values"
}
struct ResponseValue: Codable {
var title: String
var text: String
enum CodingKeys: String, CodingKey {
case title = "Title"
case text = "Text"
}
}
}
}