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`
Related
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)
}
}
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.
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)
}
I am working on deserializing JSON with SwiftyJSON and Swift 5.1.
I have a set of data like below
"name": "American Standard",
"number": 1,
"subcategories": [
{
"name": "American Light Lager",
"letter": "A",
"guidelines": {
"overallImpression": "Some String",
"aroma": "Some String",
"appearance": "Some String",
"flavor": "Some String",
"mouthfeel": "Some String",
"comments": "Some String",
"history": "Some String",
"ingredients": "Some String",
"comparison": "Some String",
"vitalStatistics": {
"og": "1.028 - 1.040",
"fg": "0.998 - 1.008",
"abv": "2.8 - 4.2%",
"ibu": "8 - 12",
"srm": "2 - 3"
}
},
"commercialExamples": [
{
"name": "name1"
},
{
"name": "name2"
},
{
"name": "name3"
},
],
"tags": [
{
"tag": "tag1"
},
{
"tag": "tag2"
},
]
},
I am using struct to hold all the data, shown below.
struct Beers
{
var name: String
var number: Int
var subcategories: Subcategory
}
struct Subcategory
{
var name: String
var letter: String
var guidelines: Guidelines
var commercialExamples: [CommercialExample]
var tags: [Tag]
}
struct Guidelines
{
var overallImpression: String
var aroma: String
var appearance: String
var flavor: String
var mouthfeel: String
var comments: String
var history: String
var ingredients: String
var comparison: String
var vitalStatistics: VitalStatistics
}
struct VitalStatistics
{
var og: String
var fg: String
var abv: String
var ibu: String
var srm: String
}
struct CommercialExample : Hashable
{
var name: String
func hash(into hasher: inout Hasher)
{
hasher.combine(name)
}
}
struct Tag : Hashable
{
var tag: String
func hash(into hasher: inout Hasher)
{
hasher.combine(tag)
}
}
And for my deserialization code, I have this.
for (index, dict) in jsonObject
{
let thisBeer = Beers(name: dict["name"].stringValue, number: dict["number"].intValue, subcategories: Subcategory(name: dict["name"].stringValue, letter: dict["letter"].stringValue, guidelines: Guidelines(overallImpression: dict["overallImpression"].stringValue, aroma: dict["aroma"].stringValue, appearance: dict["appearance"].stringValue, flavor: dict["flavor"].stringValue, mouthfeel: dict["mouthfeel"].stringValue, comments: dict["comments"].stringValue, history: dict["history"].stringValue, ingredients: dict["ingredients"].stringValue, comparison: dict["comparison"].stringValue, vitalStatistics: VitalStatistics(og: dict["og"].stringValue, fg: dict["fg"].stringValue, abv: dict["abv"].stringValue, ibu: dict["ibu"].stringValue, srm: dict["srm"].stringValue)), commercialExamples: CommercialExample(name: dict["name"].stringValue), tags: Tag(tags: dict["tags"].stringValue)))
beers.append(thisBeer)
}
This is where I am stuck. I am much more familiar with C# and .net. I just don't know how to go about looping through the commercialExamples and tags and creating the objects from the data and populating the arrays with them. What is the proper way of doing this with Swift and SwiftyJSON?
You must first use JSONSerializable protocols to decode your JSON data into the object. For example in your case:
struct Beers: JSONSerializable {
var name: String?
var number: Int?
var subcategories: Subcategory?
enum CodingKeys: String, CodingKey {
case name
case number
case subcategories
}
}
extension Beers: Decodable {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
if let value = try values.decodeIfPresent(String?.self, forKey: .name) {
if let value = value {
name = value
}
}
if let value = try values.decodeIfPresent(Int?.self, forKey: .number) {
if let value = value {
number = value
}
}
if let value = try values.decodeIfPresent(Subcategory?.self, forKey: .subcategories) {
if let value = value {
subcategories = value
}
}
}
}
Then you can make an object from JSON data like this:
do {
let model = try JSONDecoder().decode(Beers.self, from: jsonData)
} catch let error {
print(error)
}
I have a JSON object from Yelp and I cannot figure out how to access the title in categories using Swift Codable. This is the JSON (with some elements removed for ease of reading):
{
"businesses": [
{
"id": "fob-poke-bar-seattle-2",
"name": "FOB Poke Bar",
"is_closed": false,
"review_count": 421,
"categories": [
{
"alias": "poke",
"title": "Poke"
},
{
"alias": "salad",
"title": "Salad"
},
{
"alias": "hawaiian",
"title": "Hawaiian"
}
],
"rating": 5,
"coordinates": {
"latitude": 47.6138005187095,
"longitude": -122.343868017197
},
"price": "$$",
"location": {
"city": "Seattle",
"zip_code": "98121",
"country": "US",
"state": "WA",
"display_address": [
"220 Blanchard St",
"Seattle, WA 98121"
]
},
}
Here it is in JSON Viewer
I access first level properties like name easily, and I access lattitude and longitude like so:
class Business: Codable {
var name: String
var rating: Double
var coordinates: Coordinates
struct Coordinates: Codable {
var latitude: Double
var longitude: Double
init(lat: Double, long: Double) {
self.latitude = lat
self.longitude = long
}
}
init(name: String, rating: Double, coordinates: Coordinates) {
self.name = name
self.rating = rating
self.coordinates = coordinates
self.categories = categories
}
}
Could anyone please point me in the right direction towards accessing categories -> title? Coordinates was easy to access but categories is an array of dictionaries. Thank you!
It's the same pattern like Coordinates except the value for categories is an array:
struct Root : Decodable {
let businesses : [Business]
}
struct Business: Decodable {
let name: String
let rating: Int
let coordinates: Coordinates
let categories : [Category]
struct Coordinates: Codable {
let latitude: Double
let longitude: Double
}
struct Category: Codable {
let alias: String
let title: String
}
}
let root = try decoder.decode(Root.self, from: data)
for business in root.businesses {
for category in business.categories {
print(category.title)
}
}