Generic UITableViewCell with Different Kind Of JSON data - ios

I like to use a single tableViewCell instance (instead creating separate cells) for different kind of data; used a simple generic approach by creating a protocol for this but how can I populate data from JSON (by avoiding switch-case) in clear way?
protocol CellData {
var title: String { get set }
var subTitle: String { get set }
var image: String { get set }
}
for the singleCell
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var subTitleLabel: UILabel!
#IBOutlet weak var imageView: UIImageView!
{
"data": [
{
"type": "company",
"data": {
"name": "Google",
"sector": "IT",
"logo": "https://www.google.com/logos/doodles/2015/googles-new-logo-5078286822539264.3-hp2x.gif"
}
},
{
"type": "person",
"data": {
"name": "Bill Gates",
"occupation": "Microsoft CEO",
"picture": "https://img.etimg.com/thumb/msid-66398917,width-640,resizemode-4,imgsize-702055/words-of-wisdom.jpg"
}
},
{
"type": "song",
"data": {
"name": "Beat It",
"singer": "M.Jackson",
"thumbnail": "https://cdn.smehost.net/michaeljacksoncom-uslegacyprod/wp-content/uploads/2019/08/Sept2019Mobile.jpg"
}
},
{
"type": "vehicle",
"data": {
"name": "Silver Silver",
"brand": "Silver",
"photo": "https://images.pexels.com/photos/112460/pexels-photo-112460.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500"
}
}
],
"error": null
}

If you have guarantees about the keys that you can expect, then you could try to parse the data for the first key that is found:
struct CellDataEnvelope: Decodable {
let data: CellData
let type: CellDataType
enum CellDataType: String, Decodable {
case company
case person
case song
case vehicle
}
}
struct CellData: Decodable {
let title: String
let subTitle: String
let image: String
enum CodingKeys: CodingKey {
// title
case name
// subTitle
case sector
case occupation
case singer
case brand
// image
case thumbnail
case picture
case logo
case photo
}
}
extension CellData {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.title = try container.decode(String.self, forKey: .name)
self.subTitle = try tryDecodeString(in: container, keys: [.sector, .occupation, .singer, .brand])
self.image = try tryDecodeString(in: container, keys: [.thumbnail, .picture, .logo, .photo])
}
}
// Attempts to decode a String value from an array of keys, returns the first one to successfully decode.
private func tryDecodeString(
in container: KeyedDecodingContainer<CellData.CodingKeys>,
keys: [CellData.CodingKeys]
) throws -> String {
for key in keys {
if let value = try? container.decode(String.self, forKey: key) {
return value
}
}
throw DecodingError.dataCorrupted(
.init(
codingPath: [],
debugDescription: "Invalid data"
)
)
}
let models = try JSONDecoder().decode([CellDataEnvelope].self, from: Data(json.utf8))
This could become unwieldy if the list of keys will grow substantially or if you don't have guarantees about them.

You can have a model with optional attributes, which means they won't always need to have all of the properties setted, but as mentioned by Vadian, at some point you will need to map what you are receiving.
You can also define all of your keys that are not image or title, as the subtitle (for example, sector or occupation or singer or brand).

Related

Elegant solutions to decoding a JSON nested array in swift using decodable

Currently working with an API, and while I have successfully gotten it to decode the full result, I am only interested in the Entities/identifier portion. While I have gotten it working and get what I want/need I feel like this could be done better and more elegant and maybe in a single step. Any insight/suggestions appreciated.
JSON returned from API:
{
"count": 4596,
"entities": [
{
"facet_ids": [
"contact",
"siftery",
"investor",
"ipqwery",
"aberdeen",
"apptopia",
"semrush",
"company",
"rank",
"builtwith",
"bombora",
"key_event"
],
"identifier": {
"uuid": "468bef9f-2f50-590e-6e78-62e3adb05aa1",
"value": "Citi",
"image_id": "v1417152861/pdgwqt8ddecult5ktvdf.jpg",
"permalink": "citigroup",
"entity_def_id": "organization"
},
"short_description": "Citigroup is a diversified financial services holding company that provides various financial products and services."
},
{
"facet_ids": [
"contact",
"siftery",
"investor",
"apptopia",
"semrush",
"company",
"rank",
"builtwith",
"key_event"
],
"identifier": {
"uuid": "031a344b-c2b9-e60b-d950-1ae062026fde",
"value": "Citi",
"image_id": "yzlzhjqpparamrswaqa1",
"permalink": "citi-2",
"entity_def_id": "organization"
},
"short_description": "CITi is an NPO supporting the ICT sector in Western Cape."
},
{
"facet_ids": [
"contact",
"siftery",
"semrush",
"company",
"rank",
"builtwith",
"bombora"
],
"identifier": {
"uuid": "7ce45379-957c-49c5-bca2-c9ffd521f7da",
"value": "CITI",
"image_id": "qbkqndm7d0wgbogxjcrs",
"permalink": "citi-f7da",
"entity_def_id": "organization"
},
"short_description": "CITI trusted gateway to project-based change expertise that major organisations need to thrive, change and innovate."
}
]
}
Structs:
struct Entity: Decodable, Identifiable
{
var id: String
var companyName: String
var permalink: String
var imageID: String
init(from entity: Entities.Entity) throws
{
self.id = entity.identifier?.uuid ?? ""
self.companyName = entity.identifier?.value ?? ""
self.permalink = entity.identifier?.permalink ?? ""
self.imageID = entity.identifier?.image_id ?? ""
}
}
struct Entities: Decodable
{
var count:Int?
var entities: [Entity]?
struct Entity: Decodable
{
var facet_ids:[String]?
var identifier:Identifier?
var short_description:String?
}
struct Identifier:Decodable
{
var permalink:String?
var value:String?
var image_id:String?
var entity_def_id:String?
var uuid:String?
}
}
Call to decode:
if let data = data{
do {
let businessEntities = try decoder.decode(Entities.self, from: data)
let entities:[Entity] = try businessEntities.entities!.compactMap{
entity in
do
{
return try Entity(from: entity)
}
}
Thinking you are just interested in the Entities/identifier, you can simplify your model. Here's an example:
typealias Entities = [Entitie]
struct Entitie: Codable {
let facetIDS: [String]
let identifier: Identifier
let shortDescription: String
enum CodingKeys: String, CodingKey {
case facetIDS = "facet_ids"
case identifier
case shortDescription = "short_description"
}
}
struct Identifier: Codable {
let uuid, value, imageID, permalink: String
let entityDefID: String
enum CodingKeys: String, CodingKey {
case uuid, value
case imageID = "image_id"
case permalink
case entityDefID = "entity_def_id"
}
}
You can access entities object and decode it like this:
guard let data = data,
let jsonDict = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let entitiesObj = jsonDict["entities"] else { return }
do {
let entitiesData = try JSONSerialization.data(withJSONObject: entitiesObj)
let result = try JSONDecoder().decode(Entities.self, from: entitiesData)
} catch {
print(error.localizedDescription)
}
Obs: I created the CodingKeys because I use camelCase in my projects, you can left it in snake_case just remember to replace variables.

Swift Decodable Request object [duplicate]

This question already has answers here:
Swift Codable multiple types
(2 answers)
Closed 1 year ago.
I have a request object that points to an API from Stripe. The response from stripe looks like this
{
"object": "list",
"url": "/v1/refunds",
"has_more": false,
"data": [
{
"id": "re_3Jkggg2eZvKYlo2C0ArxjggM",
"object": "refund",
"amount": 100,
"balance_transaction": null,
"charge": "ch_3Jkggg2eZvKYlo2C0pK8hM73",
"created": 1634266948,
"currency": "usd",
"metadata": {},
"payment_intent": null,
"reason": null,
"receipt_number": null,
"source_transfer_reversal": null,
"status": "succeeded",
"transfer_reversal": null
},
{...},
{...}
]
}
I've created a decodable Request struct that looks like this:
struct Request: Decodable {
var object: String
var url: String
var has_more: Bool
var data: [Any]
}
The issue I am having is that the Request object can have data that contains an array of several different Objects. A refund object, a card object, and others. Because of this, I've added [Any] to the request struct but am getting this error:
Type 'Request' does not conform to protocol 'Decodable'
This seems to be because decodable can't use Any as a variable type. How can I get around this and use a universal Request object with dynamic object types?
You could utilize enum to solve your problem, I have done this in the past with great success.
The code example below can be copy/pasted into the Playground for further tinkering. This is a good way increase your understanding of the inner workings when decoding JSON to Decodable objects.
Hope this can nudge you in a direction that will work in your case.
//: A UIKit based Playground for presenting user interface
import PlaygroundSupport
import Foundation
let json = """
{
"object": "list",
"url": "/v1/refunds",
"has_more": false,
"data": [
{
"id": "some_id",
"object": "refund",
"amount": 100,
},
{
"id": "some_other_id",
"object": "card",
"cardNumber": "1337 1447 1337 1447"
}
]
}
"""
struct Request: Decodable {
let object: String
let url: String
let has_more: Bool
let data: [RequestData] // A list of a type with a dynamic data property to hold object specific information.
}
// Type for objects of type 'card'.
struct Card: Decodable {
let cardNumber: String
}
// Type for objects of type 'refund'.
struct Refund: Decodable {
let amount: Int
}
// A simple enum for every object possible in the 'data' array.
enum RefundObject: String, Decodable {
case card
case refund
}
// An enum with associated values, mirroring the cases from RefundObject.
enum RefundData: Decodable {
case card(Card)
case refund(Refund)
}
// The base data object in the 'data' array.
struct RequestData: Decodable {
let id: String // Common properties can live in the outer part of the data object.
let object: RefundObject
let innerObject: RefundData // An enum that contain any of the cases defined within the RefundData enum.
enum CodingKeys: String, CodingKey {
case id
case object
case data
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.object = try container.decode(RefundObject.self, forKey: .object)
// Here we decode id (I assumed it was a common property), and object.
// The object will be used to determine what the inner part of the data object should be.
switch object {
case .card:
// Set innerObject to the .card case with an associated value of Card.
self.innerObject = .card(
try Card(from: decoder)
)
case .refund:
// Set innerObject to the .refund case with an associated value of Refund.
self.innerObject = .refund(
try Refund(from: decoder)
)
}
}
}
let decoder = JSONDecoder()
let requestData = try decoder.decode(Request.self, from: json.data(using: .utf8)!)
// Example usage of the innerObject:
for data in requestData.data {
switch data.innerObject {
case .card(let card):
print(card.cardNumber)
case .refund(let refund):
print(refund.amount)
}
}
https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html
https://developer.apple.com/documentation/foundation/jsondecoder
struct Request: Decodable {
var object: String
var url: String
var has_more: Bool
var data: [MyData]
}
then you have also make a decodable struct for that data array
struct MyData:Decodable {
var id: String
var object: String
var amount:Int
balance_transaction:String?
....
}

using decodable to capture array of nested objects in single line

Below is my json response and the struct which I need to costruct out of it.
condition: I would not like to create any other struct apart from Response,Media and would like to parse in single line as specified below.
{
"name": "xxxx",
"title": "xxxxxxx",
"assets": [
{
"items": [
{
"id": "eeee",
"desc": "rrrrrr"
}, {
"id": "eeee",
}, {
"desc": "rrrrrr"
}]
}]
}
struct Response {
let name : String
let title : string
let items : [Media]
private enum codingKeys : String, CodingKey {
case name = "name"
case title = "title"
case items = "assets.items"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: codingKeys.self)
name = try container.decode(String.self, forKey: .name)
title = try container.decode(TrayLayout.self, forKey: .title)
items = try container.decode([Media].self, forKey: .items)
}
}
I managed to find a solution for your problem.
Considering following is sample json
let jsonString = """
{
"name": "xxxx",
"title": "xxxxxxx",
"assets": [
{
"items": [
{
"id": "id11",
"desc": "desc11"
}, {
"id": "id12",
}, {
"desc": "desc13"
}]
},{
"items": [
{
"id": "id21",
"desc": "desc21"
}, {
"id": "id22",
}, {
"desc": "desc23"
}]
}]
}
"""
Your structures will look as below
struct Media: Codable {
let id,desc: String?
enum CodingKeys: String, CodingKey {
case id,desc
}
}
struct Response: Decodable {
let name,title: String?
let items: [Media]?
enum CodingKeys: String, CodingKey {
case name,title,items
case assets = "assets"
}
// Decoding
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
title = try container.decode(String.self, forKey: .title)
// Here as the assets contains array of object which has a key as items and then again consist of a array of Media we need to decode as below
let assets = try container.decode([[String:[Media]]].self, forKey: .assets)
// At this stage will get assets with a array of array so we first map and retrive the items and then reduce them to one single array
items = assets.compactMap{$0[CodingKeys.items.rawValue]}.reduce([], +)
}
}
At the end it's time to use it as follows
let data = jsonString.data(using: .utf8)!
let myResponse = try! JSONDecoder().decode(Response.self, from: data)
Now you can access the data as below
myResponse.name
myResponse.title
myResponse.items
Hope this basic code helps you to achieve what you want to do. And then you can go ahead and do more nested parsing(s).
I would like to thanks Nic Laughter for such a detailed article; by referring which I managed to come with above solution.
Found a new way:
struct Media: Codable {
let id,desc: String?
enum CodingKeys: String, CodingKey {
case id,desc
}
}
struct Response: Decodable, CustomStringConvertible {
let name,title: String?
#NestedKey
let items: [Media]?
enum CodingKeys: String,NestableCodingKey {
case name,title,
case items = "assets/items"
}
}
You can try this:
struct Response: Decodable {
let name, title: String
let assets: [Assets]
}
struct Assets: Decodable {
let items: [Items]
}
struct Items: Decodable {
let id, desc: String
}
Then you can just decode it like this:
guard let response = try? JSONDecoder().decode(Response.self, from: data) else { print("Response not parsed"); return }

Parse Using Swifty JSON

My JSON is like:
{
"status": 1,
"msg": "Category Product List",
"product_data": [{
"product_id": "49",
"image": "http://192.168.1.78/Linkon/site/pub/static/frontend/Linkon/default/en_US/Magento_Catalog/images/product/placeholder/image.jpg",
"shopName": "putin",
"review": "",
"rating": "2",
"productName": "ccd",
"customrFirstName": "devi",
"customrLastName": "ss",
"address": "6th Ln, S.T.Colony, Mahalaxminagar, Rajarampuri, Kolhapur, Maharashtra 416008, India",
"contactNumber": null,
"description": "<p>ccd</p>"
},
{
"product_id": "50",
"image": "http://192.168.1.78/Linkon/site/pub/static/frontend/Linkon/default/en_US/Magento_Catalog/images/product/placeholder/image.jpg",
"shopName": "putin",
"review": "",
"rating": "2",
"productName": "car garage",
"customrFirstName": "devi",
"customrLastName": "ss",
"address": "6th Ln, S.T.Colony, Mahalaxminagar, Rajarampuri, Kolhapur, Maharashtra 416008, India",
"contactNumber": null,
"description": "<p>car garage</p>"
}
]
}
So my question is: How to create JSON model class and parse using swifty JSON?
I would recommend ditching SwiftyJSON in favor of the built-in Codable and JSONDecoder support in Swift 4.
For this, you simply define a struct that matches your JSON format, and decode it:
struct Data: Codable {
let status: Int
let msg: String
let products: [Product]
enum CodingKeys: String, CodingKey {
case status, msg
case products = "product_data"
}
}
struct Product: Codable {
let product_id, image, shopName, review: String
let rating, productName, customrFirstName, customrLastName: String
let address: String
let contactNumber: String?
let description: String
}
do {
let data = try JSONDecoder().decode(Data.self, from: json)
print("\(data.msg)") // e.g.
} catch {
print("\(error)")
}
You can create your data model class like below:
import UIKit
import SwiftyJSON
class ProductModels: NSObject {
var productModel:[ProductModel]?
}
public init(json:JSON) {
self.productModel = json["product_data"].dictionary
}
class ProductModel: NSObject {
var productID:String?
var image:String?
var shopName:String?
var review:String?
var rating:String?
var productName:String?
var customrFirstName:String?
var customrLastName:String?
var address:String?
var contactNumber:String?
var description:String?
public init(json:JSON) {
self.productID = json["product_id"].string
self. image = json["image"].string
self. shopName = json["shopName"].string
self. review = json["review"].string
self. rating = json["rating"].string
self. productName = json["productName"].string
self. customrFirstName = json["customrFirstName"].string
self. customrLastName = json["customrLastName"].string
self. address = json["address"].string
self. contactNumber = json["contactNumber"].string
self. description = json["description"].string
}
}
and can use this class by passing response from model who is calling api and getting this response example below: (the class where you are using this below code, you have to import SwiftyJSON)
Alamofire.request(//(your method or api call).responseJSON(completionHandler: { (response) in
switch response.result {
case .success(let value):
let productJson = JSON(value)
let productsData = ProductModels(json: productJson)
break;
}
})
You can create class using SwiftyJSONAccelerator
Get SwiftyJSONAccelerator from Here: https://github.com/insanoid/SwiftyJSONAccelerator
or you can create it online using https://app.quicktype.io/

Modelling an 'object' type to decode JSON

I'm making multiple calls to the Apple Music API which is returning an array of Resource objects. The Resource object has a property called attributes of type object which varies depending on whether it is a Song, Playlist, or Album.
How do I model the resource object so I can decode the JSON and create Song, Playlist, and Album objects?
struct Resource {
let id : String?
let type : String?
let href : String?
let attributes : "What goes here?"
}
Update 1:
I tried Joe's second recommendation and set up my structs like so:
struct Resource: Decodable{
enum Attribute {
case song(Song), playlist(Playlist)
}
let id : String?
let type : String?
let href : String?
let attributes: Attribute?
enum CodingKeys : CodingKey{
case id, type, href, attributes
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(String.self, forKey: .id)
type = try values.decode(String.self, forKey: .type)
href = try values.decode(String.self, forKey: .href)
switch type {
case "library-songs":
attributes = try .song(values.decode(Song.self, forKey: .attributes))
case "library-playlists":
attributes = try .playlist(values.decode(Playlist.self, forKey: .attributes))
default:
attributes = nil
}
}
}
struct Song : Decodable {
let playParams : PlayParams?
let trackNumber : Int?
let durationInMillis : Int?
let name : String?
let albumName : String?
let artwork : Artwork?
let contentRating : String?
let artistName : String?
}
struct Artwork : Decodable {
let width : Int?
let height : Int?
let url : String?
}
struct PlayParams : Decodable {
let id : String?
let kind : String?
let isLibrary : Bool?
}
The sample JSON looks like this for song:
{
"data": [
{
"id": "i.4YBNbl3IXVJQRM",
"type": "library-songs",
"href": "/v1/me/library/songs/i.4YBNbl3IXVJQRM",
"attributes": {
"albumName": "\"Awaken, My Love!\"",
"artwork": {
"width": 1200,
"height": 1200,
"url": "https://is5-ssl.mzstatic.com/image/thumb/Music71/v4/00/d0/d7/00d0d743-b0de-31d8-09eb-0796269bb555/UMG_cvrart_00044003187658_01_RGB72_1800x1800_16UMGIM77118.jpg/{w}x{h}bb.jpg"
},
"durationInMillis": 326933,
"playParams": {
"id": "i.4YBNbl3IXVJQRM",
"kind": "song",
"isLibrary": true
},
"artistName": "Childish Gambino",
"trackNumber": 6,
"name": "Redbone",
"contentRating": "explicit"
}
},
{
"id": "i.mmpeOrZiLqoKOv",
"type": "library-songs",
"href": "/v1/me/library/songs/i.mmpeOrZiLqoKOv",
"attributes": {
"albumName": "Funk Wav Bounces Vol. 1",
"artwork": {
"width": 1200,
"height": 1200,
"url": "https://is5-ssl.mzstatic.com/image/thumb/Music127/v4/8b/37/23/8b372308-f764-d03a-5bda-a7a456292547/886446469607.jpg/{w}x{h}bb.jpg"
},
"durationInMillis": 272659,
"playParams": {
"id": "i.mmpeOrZiLqoKOv",
"kind": "song",
"isLibrary": true
},
"artistName": "Calvin Harris",
"trackNumber": 4,
"name": "Rollin (feat. Future & Khalid)",
"contentRating": "explicit"
}
},
{
"id": "i.JL1aVxNtzmYDJG",
"type": "library-songs",
"href": "/v1/me/library/songs/i.JL1aVxNtzmYDJG",
"attributes": {
"albumName": "The Weekend (Funk Wav Remix) - Single",
"artwork": {
"width": 1200,
"height": 1200,
"url": "https://is4-ssl.mzstatic.com/image/thumb/Music118/v4/aa/d5/e5/aad5e5e3-dff5-7d8f-5747-b695ad9f2299/886446852157.jpg/{w}x{h}bb.jpg"
},
"durationInMillis": 171806,
"playParams": {
"id": "i.JL1aVxNtzmYDJG",
"kind": "song",
"isLibrary": true
},
"artistName": "SZA & Calvin Harris",
"trackNumber": 1,
"name": "The Weekend (Funk Wav Remix)"
}
}
]
}
When I decode the data using:
let resource = try? JSONDecoder().decode([Resource].self, from: data!)
print(resource!)
I get a fatal error: Unexpectedly found nil while unwrapping an Optional value
Update 2:
I figured out the issue. The extra layer on top was screwing up the decoding. I added another level:
struct Welcome: Codable {
let data: [Datum]
}
struct Datum: Codable {
let id, type, href: String
let attributes: Attributes
}
struct Attributes: Codable {
let albumName: String
let artwork: Artwork
let durationInMillis: Int
let playParams: PlayParams
let artistName: String
let trackNumber: Int
let name: String
let contentRating: String?
}
struct Artwork: Codable {
let width, height: Int
let url: String
}
struct PlayParams: Codable {
let id, kind: String
let isLibrary: Bool
}
This just works for songs but I will try reimplementing for both songs and playlists.

Resources