Swift 5 - Display nested JSON with Arrays of Objects - arrays

I'm trying to figure out the correct Syntax (or where I am going wrong?) for displaying returned multi nested json that has arrays containing further objects.
I can manage to get the nested objects to work but I get an error when trying to get the objects inside the array.
Here is my Struct:
import Foundation
struct spaceXLaunch: Codable {
var flight_number: Int
var mission_name: String?
var launch_date_local: String?
var rocket: rocketResponse
var launch_site: launchSiteResponse
var links: linksResponse
var launch_success: Bool?
var details: String?
}
extension spaceXLaunch: Identifiable {
var id: Int { return flight_number }
}
struct rocketResponse: Codable {
var rocket_name: String
var rocket_id: String
var first_stage: firstStageResponse
var second_stage: secondStageResponse
}
struct firstStageResponse: Codable {
var cores: [coresResponse]
}
struct coresResponse: Codable {
var landing_vehicle: String?
var land_success: Bool?
var reused: Bool?
var landing_type: String?
}
struct secondStageResponse: Codable {
var payloads: [payloadsResponse]
}
struct payloadsResponse: Codable {
var payload_id: String?
var payload_type: String?
var payload_mass_kg: Double?
}
struct launchSiteResponse: Codable {
var site_name: String
}
struct linksResponse: Codable {
var missionPatch: String?
var missionPatchSml: String?
private enum CodingKeys: String, CodingKey {
case missionPatch = "mission_patch"
case missionPatchSml = "mission_patch_small"
}
}
Here is the SwiftUI code Im using to render the view
import SwiftUI
struct listView: View {
#ObservedObject var fetcher = LaunchDataFetcher()
var body: some View {
List(fetcher.launches) { spaceXLaunch in
VStack (alignment: .leading) {
Text(spaceXLaunch.mission_name ?? "No Data Found")
Spacer()
Text(spaceXLaunch.details ?? "No Data Found")
.font(.system(size: 11))
Spacer()
Text(spaceXLaunch.launch_date_local ?? "No Data Found")
Spacer()
Text(spaceXLaunch.rocket.rocket_name)
Spacer()
///This line below comes back with an error of "Cannot subscript a value of incorrect or ambiguous type"
Text(spaceXLaunch.rocket.first_stage.cores[0].landing_type)
Spacer()
}
}
}
}
struct listView_Previews: PreviewProvider {
static var previews: some View {
listView()
.environment(\.colorScheme, .dark)
}
}
Here is a sample of the Json data returned for the api
{
"flight_number": 24,
"mission_name": "CRS-7",
"mission_id": [
"EE86F74"
],
"launch_year": "2015",
"launch_date_unix": 1435501260,
"launch_date_utc": "2015-06-28T14:21:00.000Z",
"launch_date_local": "2015-06-28T10:21:00-04:00",
"is_tentative": false,
"tentative_max_precision": "hour",
"tbd": false,
"launch_window": 0,
"rocket": {
"rocket_id": "falcon9",
"rocket_name": "Falcon 9",
"rocket_type": "v1.1",
"first_stage": {
"cores": [
{
"core_serial": "B1018",
"flight": 1,
"block": 1,
"gridfins": true,
"legs": true,
"reused": false,
"land_success": null,
"landing_intent": true,
"landing_type": "ASDS",
"landing_vehicle": "OCISLY"
}
]
},
"second_stage": {
"block": 1,
"payloads": [
{
"payload_id": "CRS-7",
"norad_id": [],
"reused": false,
"cap_serial": "C109",
"customers": [
"NASA (CRS)"
],
"nationality": "United States",
"manufacturer": "SpaceX",
"payload_type": "Dragon 1.1",
"payload_mass_kg": 2477,
"payload_mass_lbs": 5460.9,
"orbit": "ISS",
"orbit_params": {
"reference_system": "geocentric",
"regime": "low-earth",
"longitude": null,
"semi_major_axis_km": null,
"eccentricity": null,
"periapsis_km": null,
"apoapsis_km": null,
"inclination_deg": 51.6,
"period_min": null,
"lifespan_years": null,
"epoch": null,
"mean_motion": null,
"raan": null,
"arg_of_pericenter": null,
"mean_anomaly": null
},
"mass_returned_kg": null,
"mass_returned_lbs": null,
"flight_time_sec": 139,
"cargo_manifest": "https://www.nasa.gov/sites/default/files/atoms/files/spacex_crs-7_mission_overview.pdf",
"uid": "UerI6qmZTU2Fx2efDFm3QQ=="
}
]
},
"fairings": null
},
"ships": [
"ELSBETH3",
"GOQUEST",
"GOSEARCHER"
],
"telemetry": {
"flight_club": null
},
"launch_site": {
"site_id": "ccafs_slc_40",
"site_name": "CCAFS SLC 40",
"site_name_long": "Cape Canaveral Air Force Station Space Launch Complex 40"
},
"launch_success": false,
"launch_failure_details": {
"time": 139,
"altitude": 40,
"reason": "helium tank overpressure lead to the second stage LOX tank explosion"
},
"links": {
"mission_patch": "https://images2.imgbox.com/47/39/stH98Qy1_o.png",
"mission_patch_small": "https://images2.imgbox.com/d0/22/gyTVYo21_o.png",
"reddit_campaign": null,
"reddit_launch": "https://www.reddit.com/r/spacex/comments/3b27hk",
"reddit_recovery": null,
"reddit_media": "https://www.reddit.com/r/spacex/comments/3berj3",
"presskit": "https://www.nasa.gov/sites/default/files/atoms/files/spacex_nasa_crs-7_presskit.pdf",
"article_link": "https://spaceflightnow.com/2015/06/28/falcon-9-rocket-destroyed-in-launch-mishap/",
"wikipedia": "https://en.wikipedia.org/wiki/SpaceX_CRS-7",
"video_link": "https://www.youtube.com/watch?v=PuNymhcTtSQ",
"youtube_id": "PuNymhcTtSQ",
"flickr_images": [
"https://farm1.staticflickr.com/344/19045370790_f20f29cd8d_o.jpg",
"https://farm1.staticflickr.com/287/18999110808_6e153fed64_o.jpg"
]
},
"details": "Launch performance was nominal until an overpressure incident in the second-stage LOX tank, leading to vehicle breakup at T+150 seconds. The Dragon capsule survived the explosion but was lost upon splashdown because its software did not contain provisions for parachute deployment on launch vehicle failure.",
"upcoming": false,
"static_fire_date_utc": "2015-06-26T05:00:00.000Z",
"static_fire_date_unix": 1435294800,
"timeline": {
"webcast_liftoff": 61,
"go_for_prop_loading": -2280,
"rp1_loading": -2100,
"stage1_lox_loading": -2100,
"stage2_lox_loading": -960,
"engine_chill": -420,
"prelaunch_checks": -60,
"propellant_pressurization": -60,
"go_for_launch": -45,
"ignition": -3,
"liftoff": 0,
"maxq": 60,
"meco": 180,
"stage_sep": 180,
"second_stage_ignition": 180,
"seco-1": 540,
"dragon_separation": 600,
"dragon_solar_deploy": 720,
"dragon_bay_door_deploy": 8400
},
"crew": null
},

Thanks to Chris for pointing out that adding an optional (??) at the end of this line
Text(spaceXLaunch.rocket.first_stage.cores[0].landing_type)
so it looks like this:
Text(spaceXLaunch.rocket.first_stage.cores[0].landing_type ?? "No Data Found")
and it all works.

Generate your JSONModel structs with following tool https://app.quicktype.io . Then validated your logic.

Related

How to parse array data from Yelp API?

Allo, Hi, im new with SwiftUI and im facing some issue with the Yelp API and can't find answer anywhere... I'm creating a restaurant app and I want to add a on the business detail page a list of Yelp transactions that the business is registered for ("pickup", "delivery", and "restaurant_reservation").
I've try a lot of way to retrieved it but im about to give up... I don't know if it's me who's dumb or anything but my brain can't figured it out anymore. I've tried to get the data with "ForEach" and all any other way we usually get array data...
Second question (similar as the previous one) how can I retrieve the category alias/title from the API? I want to be able to filter the business based on their categories and also show on the business detail page the category associated to it.
Thank you :)
Yelp Response Body Example :
{
"total": 144,
"businesses": [
{
"id": "gR9DTbKCvezQlqvD7_FzPw",
"alias": "north-india-restaurant-san-francisco",
"price": "$$",
"url": "https://www.yelp.com/biz/north-india-restaurant-san-francisco",
"rating": 4,
"location": {
"zip_code": "94105",
"state": "CA",
"country": "US",
"city": "San Francisco",
"address2": "",
"address3": "",
"address1": "123 Second St"
},
"categories": [
{
"alias": "indpak",
"title": "Indian"
}
],
"phone": "+14153481234",
"coordinates": {
"longitude": -122.399305736113,
"latitude": 37.787789124691
},
"image_url": "http://s3-media4.fl.yelpcdn.com/bphoto/howYvOKNPXU9A5KUahEXLA/o.jpg",
"is_closed": false,
"name": "North India Restaurant",
"review_count": 615,
"transactions": ["pickup", "restaurant_reservation"]
},
// ...
]
}
Here is my Business model :
class Business: Decodable, Identifiable, ObservableObject {
#Published var imageData: Data?
var id: String?
var alias: String?
var name: String?
var imageUrl: String?
var isClosed: Bool?
var url: String?
var reviewCount: Int?
var categories: [Category]?
var rating: Double?
var coordinates: Coordinate?
var transactions: [String]?
var price: String?
var location: Location?
var phone: String?
var displayPhone: String?
var distance: Double?
enum CodingKeys: String, CodingKey {
case imageUrl = "image_url"
case isClosed = "is_closed"
case reviewCount = "review_count"
case displayPhone = "display_phone"
case id
case alias
case name
case url
case categories
case rating
case coordinates
case transactions
case price
case location
case phone
case distance
}
func getImageData() {
// Check that image url isn't nil
guard imageUrl != nil else {
return
}
// Download the data for the image
if let url = URL(string: imageUrl!) {
// Get a session
let session = URLSession.shared
let dataTask = session.dataTask(with: url) { (data, response, error) in
if error == nil {
DispatchQueue.main.async {
// Set the image data
self.imageData = data!
}
}
}
dataTask.resume()
}
}
static func getTestData() -> Business {
let b = Business()
return b
}
}
struct Category: Decodable {
var alias: String?
var title: String?
}
Here an example of my code :
struct BusinessDetail: View {
var business: Business
#State private var showDirections = false
var body: some View {
VStack (alignment: .leading) {
VStack (alignment:.leading, spacing:0) {
GeometryReader() { geometry in
// Business image
let uiImage = UIImage(data: business.imageData ?? Data())
Image(uiImage: uiImage ?? UIImage())
.resizable()
.scaledToFill()
.frame(width: geometry.size.width, height: geometry.size.height)
.clipped()
}
.ignoresSafeArea(.all, edges: .top)
// Open / closed indicator
ZStack (alignment: .leading) {
Rectangle()
.frame(height: 35)
.foregroundColor(business.isClosed! ? Color("icon-primary") : Color("background"))
Text(business.isClosed! ? "Closed" : "Open")
.foregroundColor(.white)
.font(.textHeader)
.padding(.leading)
}
}
Group {
HStack {
BusinessTitle(business: business)
.padding()
Spacer()
}
// Phone
HStack {
Text("Phone:")
.bold()
Text(business.displayPhone ?? "")
Spacer()
Link("Call", destination: URL(string: "tel:\(business.phone ?? "")")!)
}
.padding()
// Transactions
if business.transactions != nil {
ForEach(business.transactions!, id: \.self) { transaction in
Text(transaction)
.font(.bodyParagraph)
}
}

swift change struct model into another model if have similar id

I get the response from the api as in the JSON code below
{
"timestamp": 1632838801,
"status": "ok",
"message": "ok",
"data": [
{
"id": 1,
"id_category": 6,
"products": [
{
"id": 12,
"product_name": "product one",
}
]
},
{
"id": 2,
"id_category": 6,
"products": [
{
"id": 20,
"product_name": "product two"
}
]
},
{
"id": 3,
"id_category": 13,
"products": [
{
"id": 994,
"product_name": "product three"
}
]
}
]
}
I'm success to decode the response into a struct like this:
struct ProductDataResponse {
let timestamp: Int
let status: Int
let message: String
let data: [Data]
}
struct Data {
let id: Int
let idCategory: Int
let products: [Product]
}
struct Product {
let id: Int
let productName: String
let idJelajah: Int
}
the problem is, I want my data model different from the response, if the data has the same id_cateogry then the existing product data will be grouped into the same id_category,below i attach the expected result of struct and the expected json.
the result is 2 array of diffrent id_category and product
struct ListCategory {
let idCategory
let category: [Category]
}
struct Category {
var id: Int
var product: [Product]
}
struct Product {
let id: Int
let productName: String
let idJelajah: Int
}
{
"timestamp": 1632838801,
"status": "ok",
"message": "ok",
"data": [
{
"id_category": 6,
"products": [
{
"id": 12,
"product_name": "product one",
},
{
"id": 20,
"product_name": "product two"
}
]
},
{
"id_category": 13,
"products": [
{
"id": 994,
"product_name": "product three"
}
]
},
]
}
With model being:
struct ProductDataResponse: Codable {
let timestamp: Double
let status: String
let message: String
let data: [Data]
struct Data: Codable {
let id: Int
let idCategory: Int
let products: [Product]
}
struct Product: Codable {
let id: Int
let productName: String
}
}
struct ProductDataOutput: Codable {
let timestamp: Double
let status: String
let message: String
let data: [Category]
struct ListCategory: Codable {
let idCategory: Int
let category: [Category]
}
struct Category: Codable {
var id: Int
var product: [Product]
}
struct Product: Codable {
let id: Int
let productName: String
}
}
Side note, to be working with your current code, I changed timestamp into a Double & status into a String, else it would cause decoding issue.
I also put them inside each other, to avoid naming. For instance, you'd need to differentiate Swift.Data & ProductDataResponse.Data, and the two Products would collide.
Also, I added a top level structure that you didn't give: ProductDataOutput.
And a helper for converting Product into Product (ie the one with structure of the response, and the one with the target one). Of course, we could use the same struct, but I wanted to separate them, just in case as you wrote it yourself.
extension ProductDataOutput.Product {
init(with otherProduct: ProductDataResponse.Product) {
self.id = otherProduct.id
self.productName = otherProduct.productName
}
}
The following code is inside a
do {
//HERE
} catch {
print("Error: \(error)")
}
That I'll skip, but keep it for debugging and catch issue.
Let's parse into the model as the Response first:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let parsed = try decoder.decode(ProductDataResponse.self, from: Data(initialJSONStr.utf8))
print(parsed)
Then, I'd use reduce(into:_:) to group by category id the products.
let reduced = parsed.data.reduce(into: [Int: [ProductDataOutput.Product]]()) { partialResult, current in
var existing = partialResult[current.idCategory, default: []]
let newProducts = current.products.map { ProductDataOutput.Product(with: $0) }
existing.append(contentsOf: newProducts)
partialResult[current.idCategory] = existing
}
print(reduced)
Then, let's create our target structure. I used a quick map() to transform the reduced into a [ProductDataOutput.Category].
let output = ProductDataOutput(timestamp: parsed.timestamp,
status: parsed.status,
message: parsed.message,
data: reduced.map { ProductDataOutput.Category(id: $0.key, product: $0.value) })
Then, to debug the output as JSON (as it's the sample you gave):
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.outputFormatting = .prettyPrinted
let encodedOutput = try encoder.encode(output)
print(String(data: encodedOutput, encoding: .utf8)!)
With this, you should be fine.

Swift JSON objects decoding error. How to write it correctly?

I have a data decoding error. How to write it correctly?
typeMismatch(Swift.Array, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array but found a dictionary instead.", underlyingError: nil))
My JSON object:
struct LeagueDocument: Codable {
var id: LeagueId
var name: LeagueName
var area: Area
var startDate: String
var endDate: String
init(id: LeagueId, name: LeagueName, area: Area, startDate: String, endDate: String) {
self.id = id
self.name = name
self.area = area
self.startDate = startDate
self.endDate = endDate
}
}
enum Area: String, Codable {
case POLAND, ENGLAND, FRANCE, SPAIN, GERMANY, ITALY, EUROPE, UEFA_CHAMPIONS_LEAGUE, EUROPE_LEAGUE
}
JSON which I got from server:
[
{
"id": "ba4ddfd6-cb91-4ea7-853d-79be89917445",
"name": "Liga 1 ",
"area": "POLAND",
"startDate": "2021-02-28",
"endDate": "2022-02-28"
},
{
"id": "4061b62b-dd57-4916-adf4-e0874ec767b1",
"name": "Liga 2",
"area": "POLAND",
"startDate": "2021-02-01",
"endDate": "2021-05-11"
}
]
My code:
func getAllActiveLeagues(completion: #escaping ([LeagueDocument]) -> Void) {
(...)
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard error == nil else { print(error!.localizedDescription); return }
if data != nil {
do {
let decodedData = try decoder.decode([LeagueDocument].self, from: data!)
completion(decodedData)
} catch {
print(error)
}
} else {
print("Data is nil")
}
}.resume()

Display items from nested array in SwiftUI

I'm trying to decode Yelp's JSON. Within the file there's a nested array of categories. Some businesses have 1, others may have 3.
Here's an example
{
"businesses": [
{
"id": "4jW-ZDeCPIl9aXvTWcATlA",
"alias": "the-delaney-hotel-orlando",
"name": "The Delaney Hotel",
"image_url": "https://s3-media2.fl.yelpcdn.com/bphoto/fikUF4yC5J63f3EOCZ8uOw/o.jpg",
"is_closed": false,
"url": "https://www.yelp.com/biz/the-delaney-hotel-orlando?adjust_creative=s-hyKAjsx6P4UW-uqMn7aQ&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=s-hyKAjsx6P4UW-uqMn7aQ",
"review_count": 13,
"categories": [
{
"alias": "hotels",
"title": "Hotels"
},
{
"alias": "venues",
"title": "Venues & Event Spaces"
}
],
"rating": 5.0,
...etc
}
]
My data model is set up this way
struct BusinessesResponse: Codable {
enum CodingKeys: String, CodingKey {
case restaurants = "businesses"
}
let restaurants: [RestaurantResponse]
}
struct RestaurantResponse: Codable, Identifiable, Equatable {
let id: String
var name: String
var image_url: String
var is_closed: Bool
var review_count: Int
var rating: Double
var distance: Double
var price: String?
var display_phone: String?
var categories: [HotelCategory]
var coordinates: HotelCoordinates
var location: HotelLocation
var transactions: [String]
}
struct HotelCategory: Hashable, Codable {
var title: String
}
struct HotelCoordinates: Hashable, Codable {
var latitude: Double
var longitude: Double
}
struct HotelLocation: Hashable, Codable {
var address1: String
var city: String
var state: String
var zip_code: String
}
I'm able to display everything other than the first pair in the categories array. My guess is that I would need to set a ForEach statement to display all available information from that array, but I'm not sure how to set that up correctly.
Here's what I have currently in abbreviated form
var category: [HotelCategory]
var body: some View {
HStack {
Text("\(category[0].title)")
}
}
Obviously that would only return the first child of that nested array. How would I dynamically account for that nested array having multiple children and then displaying them?
You can use ForEach this way:
ForEach(category, id: \.self) { category in
HStack {
Text("\(category.title)")
}
}
Note: if you're not sure that categories are unique, it might be better to conform HotelCategory to Identifiable and use id: \.id.
I'd also recommend following the convention and naming arrays with plural names, i.e.:
var categories: [HotelCategory] // instead of `category`

SwiftUI - Loop through and list items in nested array in Text element

I'm using Swift to call Yelp's API to return local business information. In the JSON response, there are multiple nested arrays, i.e. "Category" that describes what type of business it is. I'm able to return the first item in the array, but I'm lost on how to use ForEach to return each individual item in the array.
The response looks like this:
{
"businesses": [
"id": XP5BuE79...,
"categories" : [
{
"alias": "golfcourse",
"title": "Golf Course"
},
{
"alias": "clubhouse",
"title": "Clubhouse"
}
],
....
}
My data model
struct BusinessesResponse: Codable {
enum CodingKeys: String, CodingKey {
case restaurants = "businesses"
}
let restaurants: [RestaurantResponse]
}
struct RestaurantResponse: Codable, Identifiable {
let id: String
var name: String
var image_url: String
var is_closed: Bool
var review_count: Int
var rating: Double
var distance: Double
var price: String?
var display_phone: String
var categories: [RestaurantCategory]
}
Currently, I'm able to return the first item in the "categories" array by using
VStack {
ForEach(fetcher.businesses) { restaurant in
VStack (alignment: .leading) {
WebImage(url: URL(string: restaurant.image_url))
.resizable()
.indicator(.activity) // Activity Indicator
.transition(.fade(duration: 0.5)) // Fade Transition with duration
.scaledToFill()
.frame(width: 400, height: 200, alignment: .center)
.clipped()
Text(restaurant.name)
.fontWeight(.bold)
HStack(spacing: 5.0) {
Text(restaurant.price ?? "No Pricing Info")
.font(.caption)
Text("·")
//Category displaying only first in nested array
Text(restaurant.categories[0].title)
.font(.caption)
Text("·")
Image(systemName: "star.fill")
.font(.caption)
Text("\(restaurant.rating, specifier: "%.1f")")
.font(.caption)
Text("(\(restaurant.review_count))")
.font(.caption)
Spacer()
}
}
}.padding(.bottom)
.padding(.horizontal, 20)
}

Resources