Swift 4 - Nesting JSON Response for Class Structure into TableViewCell - ios

I've got the following structures to aid in returning data from a JSON web api:
// To parse the JSON, add this file to your project and do:
//
// let story = try? JSONDecoder().decode(Story.self, from: jsonData)
import Foundation
typealias Story = [StoryElement]
struct StoryElement: Codable {
let id: Int
let url: String
let storyPublic, featured: Bool
let added, modified: String
let itemType: ItemType
let collection: JSONNull?
let owner: Owner
let files: Files
let tags: [ItemType]
let elementTexts: [ElementText]
let extendedResources: ExtendedResources
enum CodingKeys: String, CodingKey {
case id, url
case storyPublic = "public"
case featured, added, modified
case itemType = "item_type"
case collection, owner, files, tags
case elementTexts = "element_texts"
case extendedResources = "extended_resources"
}
}
struct ElementText: Codable {
let html: Bool
let text: String
let elementSet: ElementSet
let element: Element
enum CodingKeys: String, CodingKey {
case html, text
case elementSet = "element_set"
case element
}
}
struct Element: Codable {
let id: Int
let url, name: String
let resource: ElementResource
}
enum ElementResource: String, Codable {
case elements = "elements"
}
struct ElementSet: Codable {
let id: Int
let url: URL
let name: Name
let resource: ElementSetResource
}
enum Name: String, Codable {
case dublinCore = "Dublin Core"
case itemTypeMetadata = "Item Type Metadata"
}
enum ElementSetResource: String, Codable {
case elementSets = "element_sets"
}
enum URL: String, Codable {
case httpWWWRalstoncemeteryCOMGreeleyAPIElementSets1 = "http://www.ralstoncemetery.com/greeley/api/element_sets/1"
case httpWWWRalstoncemeteryCOMGreeleyAPIElementSets3 = "http://www.ralstoncemetery.com/greeley/api/element_sets/3"
}
struct ExtendedResources: Codable {
let exhibitPages: Files
let geolocations: Owner
enum CodingKeys: String, CodingKey {
case exhibitPages = "exhibit_pages"
case geolocations
}
}
struct Files: Codable {
let count: Int
let url, resource: String
}
struct Owner: Codable {
let id: Int
let url, resource: String
}
struct ItemType: Codable {
let id: Int
let url, name, resource: String
}
// MARK: Encode/decode helpers
class JSONNull: Codable {
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()
}
}
You can see the JSON output I am working with, as well as the original structures here: https://app.quicktype.io?share=oAMNjooSzgpraWpf1KIj (just for reference).
The data (I believe) is returned successfully and parsed correctly, the only issue comes down to myself not being as familiar (I'm in the process of trying to learn a bit more).
So I have a TableViewController and in that I have a cell with the following coded into it:
struct StoryCellViewModel {
let id: Int
let url: String
let storyPublic, featured: Bool
let added, modified: String
}
And in the actual TableViewController under the viewDidLoad() I have this portion of script:
print(story)
self.cellViewModels = story.map{
StoryCellViewModel(id: $0.id, url: $0.url, storyPublic: $0.storyPublic, featured: $0.featured, added: $0.added, modified: $0.modified)
}
and a little bit lower than that, I have:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "StoryCell", for: indexPath)
let cellViewModel = cellViewModels[indexPath.row]
cell.textLabel?.text = String(cellViewModel.id)
cell.detailTextLabel?.text = cellViewModel.modified
return cell
}
I will make it known that all of this works, the id and modified (both just being used as tests to ensure the connection and printing works properly) are fine, but my question comes up here:
If we go back to the structures there is this portion:
struct ElementText: Codable {
let html: Bool
let text: String
let elementSet: ElementSet
let element: Element
enum CodingKeys: String, CodingKey {
case html, text
case elementSet = "element_set"
case element
}
}
Which refers to the following portion of example JSON:
"element_texts": [
{
"html": false,
"text": "Woehler and Force Farm Equipment Building",
"element_set": {
"id": 1,
"url": "http://www.ralstoncemetery.com/greeley/api/element_sets/1",
"name": "Dublin Core",
"resource": "element_sets"
},
"element": {
"id": 50,
"url": "http://www.ralstoncemetery.com/greeley/api/elements/50",
"name": "Title",
"resource": "elements"
}
},
{
"html": false,
"text": "Woehler and Force Farm Equipment",
"element_set": {
"id": 1,
"url": "http://www.ralstoncemetery.com/greeley/api/element_sets/1",
"name": "Dublin Core",
"resource": "element_sets"
},
"element": {
"id": 39,
"url": "http://www.ralstoncemetery.com/greeley/api/elements/39",
"name": "Creator",
"resource": "elements"
}
},
{
"html": false,
"text": "Street view of the front exterior of Woehler and Force Farm Equipment. Several automobiles are visible through the windows of the store. The alley along the side of the building is also visible. There are several signs along the front of the building reading, 'Farm Equipment,' 'Kaiser Frazer W&F,' and 'Woehler & Force.'; Verso There is a sticker with typed black ink reading, 'Woehler & Force 1316-22 8th Ave. - Greeley, CO 1947. Orig. env. says Liberty Trucker Parts Co./ 690 Lincoln St./F.V. Altwater/POB 1889/Denver, Colo.'",
"element_set": {
"id": 3,
"url": "http://www.ralstoncemetery.com/greeley/api/element_sets/3",
"name": "Item Type Metadata",
"resource": "element_sets"
},
"element": {
"id": 54,
"url": "http://www.ralstoncemetery.com/greeley/api/elements/54",
"name": "Story",
"resource": "elements"
}
}
],
**So my question is: ** How would I go about printing out say the first of the element_texts (The one that has "Woehler and Force Farm Equipment Building" set for the text field)?
If there is any further explanation required, I'll be happy to type it up. Or if anyone has any resources for this level of nesting, I'd be very grateful. Thank you -

story is an array of StoryElement (why not stories?)
The type of property elementTexts in StoryElement is an array of ElementText
So basically you need two loops to iterate over story and elementTexts
for aStory in story {
for elementText in aStory.elementTexts {
print(elementText.text)
}
}

First of all in "CellForRowat" call let cell = dequeueReusableCellWithIdentifier:#"cell" forIndexPath:indexPath etc.
then place text in cell.text

Create a array like this . var AllTexts = [String]()
you can fetch json data so many ways like JsonDecode/JSONSerialization, i am using JSONSerialization. After fetching data i am appending text values in to array.
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves) as! [String: Any]
for (key,value) in jsonResult {
print(key)
if let result2:[[String:Any]] = value as? [[String:Any]]{
for dict in result2 {
for (key,value) in dict {
if key == "text" {
self.AllTexts.append(value as! String)
}
}
}
}
}
print(self.AllTexts)
} catch {
// handle error
print(error)
}

Related

How to use Alamofire with Codable protocol and parse data

Firs time using Codable protocol so many things not clear. After going through several tutorials started with the Codable thing to parse data. Below is the code:
struct MyTasks : Codable {
var strDateDay : String = ""
var strReminder : String = ""
var strRepetitions : String = ""
var name : String = ""
var strNotes : String = ""
var strUserId : String = ""
private enum CodingKeys: String, CodingKey {
case strDateDay = "duedate"
case strReminder = "reminder"
case strRepetitions = "recurring"
case name = "name"
case strNotes = "notes"
case strUserId = "userId"
}
}
let networkManager = DataManager()
networkManager.postLogin(urlString: kGetMyTasks, header: header, jsonString: parameters as [String : AnyObject]) { (getMyTasks) in
print(getMyTasks?.name as Any) // -> for log
Common_Methods.hideHUD(view: self.view)
}
// Network manager:
func postLogin(urlString: String, header: HTTPHeaders, jsonString:[String: AnyObject], completion: #escaping (MyTasks?) -> Void) {
let apiString = KbaseURl + (kGetMyTasks as String)
print(apiString)
Alamofire.request(apiString, method: .post, parameters: jsonString , encoding: URLEncoding.default, headers:header).responseJSON
{ response in
let topVC = UIApplication.shared.keyWindow?.rootViewController
DispatchQueue.main.async {
//Common_Methods.showHUD(view: (topVC?.view)!)
}
guard let data = response.data else { return }
do {
let decoder = JSONDecoder()
let loginRequest = try decoder.decode(MyTasks.self, from: data)
completion(loginRequest)
} catch let error {
print(error)
completion(nil)
}
}
}
Now, this is the response:
keyNotFound(CodingKeys(stringValue: "strDateDay", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"strDateDay\", intValue: nil) (\"strDateDay\").", underlyingError: nil))
Below is the json response i am trying to parse:
{
"data": [
{
"userId": 126,
"name": "My task from postman ",
"notes": null,
"totalSteps": 0,
"completedSteps": 0,
"files": 0
},
{
"userId": 126,
"name": "My task from postman 1",
"notes": null,
"totalSteps": 0,
"completedSteps": 0,
"files": 0
}
]
}
I know i am missing something but even after spending more than half day i haven't properly understood what is wrong and where. Please guide.
problem is with your struct the names of the properties in the struct should be match with json data or if you want to use custom name you should use enum CodingKeys to convert them to your liking
struct MyTasks: Codable {
let data: [Datum]
}
struct Datum: Codable {
let userID: Int?
let name: String
let notes: String?
let totalSteps, completedSteps, files: Int
enum CodingKeys: String, CodingKey {
case userID = "userId"
case name, notes, totalSteps, completedSteps, files
}
}
func postLogin(urlString: String, header: HTTPHeaders, jsonString:[String: AnyObject], completion: #escaping (MyTasks?) -> Void) {
let apiString = KbaseURl + (kGetMyTasks as String)
print(apiString)
Alamofire.request(apiString, method: .post, parameters: jsonString , encoding: URLEncoding.default, headers:header).responseJSON
{ response in
let topVC = UIApplication.shared.keyWindow?.rootViewController
DispatchQueue.main.async {
//Common_Methods.showHUD(view: (topVC?.view)!)
}
guard let data = response.data else { return }
do {
let decoder = JSONDecoder()
let loginRequest = try decoder.decode(MyTasks.self, from: data)
completion(loginRequest)
} catch let error {
print(error)
completion(nil)
}
}
}
And one more thing keep in mind that you know the exact type of notes and make it optional otherwise it rise error, there was no type so I put a optional String in there.
Hope this will help.
The problem is that in your top JSON you have a single "data" property of type array.
You are asking JSONDecoder to decode a JSON object containing only "data" property into a Swift object called "MyTasks" with the stored properties you defined (including strDateDay).
So the decoder sends you this response because he can't find the strDateDay in that top object.
You have to make a top object for deserialization. Something like :
struct MyResponse: Codable {
var data: [MyTasks]
}
Then just give it to your JSONDecoder like you have already done :
let loginRequest = try decoder.decode(MyResponse.self, from: data)
The data you send to the decoder is your full JSON stream (the data property of Alamofire's response object), and not only the "data" property of your JSON structure.
I'd suggest to use CodyFire lib cause it support Codable for everything related to requests.
Your POST request with it may look like
struct MyTasksPayload: JSONPayload {
let param1: String
let param2: String
}
struct MyTasksResponseModel: Codable {
let strDateDay: String
let strReminder: String
let strRepetitions: String
let name: String
}
let server = ServerURL(base: "https://server1.com", path: "v1")
APIRequest<[MyTasksResponseModel]>(server, "mytasks", payload: payloadModel)
.method(.post)
.onRequestStarted {
// show HUD here
}
.onError {
// hide hud and show error here
}
.onSuccess { tasks in
// here's your decoded tasks
// hide hud here
}
Use APIRequest<[MyTasksResponseModel]> to decode array
Use APIRequest to decode one object

Parse Json array without keys swift 4

I am stuck on parsing JSON. The structure is really hard. I was trying this with a decodable approach.
import UIKit
struct WeatherItem: Decodable {
let title: String?
let value: String?
let condition: String?
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
print("hello")
let jsonUrlString = "http://virtualflight.ddns.net/api/weather.php?icao=ehrd"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
var arr = [WeatherItem]()
do {
let res = try JSONDecoder().decode([String:[[String]]].self, from: data)
let content = res["title"]!
content.forEach {
if $0.count >= 3 {
arr.append(WeatherItem(title:$0[0],value:$0[1],condition:$0[2]))
}
}
print(arr)
} catch {
print(error)
}
}
}
}
The json is the following:
{
"temperature": {
"value_c": 11,
"value_f": 285,
"condition": "Good",
"value_app": "11 \u00b0C (285 \u00b0F)"
},
"visibility": {
"value_km": 10,
"value_m": 6.2,
"condition": "Good",
"value_app": "10 KM (6.2 Mi)"
},
"pressure": {
"value_hg": 29.4,
"value_hpa": 996,
"condition": "Good",
"value_app": "29.4 inHg (996 hPa)"
},
"wind": {
"value_kts": 20,
"value_kmh": 37,
"value_heading": 280,
"condition": "Bad",
"value_app": "280\u00b0 at 20 KTS (37 Km\/H)"
},
"station": "EHRD",
"metar": "EHRD 141355Z AUTO 28020KT 250V320 9999 SCT038 BKN043 BKN048 11\/07 Q0996 NOSIG",
"remarks": "NOSIG",
"weather_page_ios_simple": [
[
"Temperature",
"11 \u00b0C (285 \u00b0F)",
"Good"
],
[
"Visibility",
"10 KM (6.2 Mi)",
"Good"
],
[
"Pressure",
"29.4 inHg (996 hPa)",
"Good"
],
[
"Wind",
"280\u00b0 at 20 KTS (37 Km\/H)",
"Bad"
],
[
"Metar",
"EHRD 141355Z AUTO 28020KT 250V320 9999 SCT038 BKN043 BKN048 11\/07 Q0996 NOSIG",
"Unknown"
],
[
"Remarks",
"NOSIG",
"Unknown"
],
[
"Station",
"EHRD",
"Unknown"
],
[
"UICell",
"iOS 12",
"siri_weather_cell"
]
]
}
any ideas how to do this?? I only need the last array, weather_page_ios_simple.
Have a look at https://app.quicktype.io it will give you the data structure for your JSON.
import Foundation
struct Welcome: Codable {
let temperature: Temperature
let visibility: Visibility
let pressure: Pressure
let wind: Wind
let station, metar, remarks: String
let weatherPageIosSimple: [[String]]
enum CodingKeys: String, CodingKey {
case temperature, visibility, pressure, wind, station, metar, remarks
case weatherPageIosSimple = "weather_page_ios_simple"
}
}
struct Pressure: Codable {
let valueHg: Double
let valueHpa: Int
let condition, valueApp: String
enum CodingKeys: String, CodingKey {
case valueHg = "value_hg"
case valueHpa = "value_hpa"
case condition
case valueApp = "value_app"
}
}
struct Temperature: Codable {
let valueC, valueF: Int
let condition, valueApp: String
enum CodingKeys: String, CodingKey {
case valueC = "value_c"
case valueF = "value_f"
case condition
case valueApp = "value_app"
}
}
struct Visibility: Codable {
let valueKM: Int
let valueM: Double
let condition, valueApp: String
enum CodingKeys: String, CodingKey {
case valueKM = "value_km"
case valueM = "value_m"
case condition
case valueApp = "value_app"
}
}
struct Wind: Codable {
let valueKts, valueKmh, valueHeading: Int
let condition, valueApp: String
enum CodingKeys: String, CodingKey {
case valueKts = "value_kts"
case valueKmh = "value_kmh"
case valueHeading = "value_heading"
case condition
case valueApp = "value_app"
}
}
If you only need the bottom array of data, you shouldn't need to put everything into the decoded struct. Just decode the part of the response you want and pull the data from there. Also, that array of data isn't really parsing JSON without keys. Its just an array of strings, you'll have to rely on the fact that index 0 is always title, 1 is always value, and 2 is always condition. Just do some validation to make sure it fits your needs. Something like this (UNTESTED)
struct WeatherItem {
let title: String?
let value: String?
let condition: String?
init(title: String?, value: String?, condition: String?) {
self.title = title
self.value = value
self.condition = condition
}
}
struct WeatherResponse: Decodable {
var weatherItems: [WeatherItem]
private enum CodingKeys: String, CodingKey {
case weatherItems = "weather_page_ios_simple"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let weatherItemArrays = try container.decode([[String]].self, forKey: .weatherItems)
weatherItems = []
for weatherItemArray in weatherItemArrays {
var title: String?
if weatherItemArray.count > 0 {
title = weatherItemArray[0]
}
var value: String?
if weatherItemArray.count > 1 {
value = weatherItemArray[1]
}
var condition: String?
if weatherItemArray.count > 2 {
condition = weatherItemArray[2]
}
weatherItems.append(WeatherItem(title: title, value: value, condition: condition))
}
}
}
And then when you get your api response get the weather items out with something like
do {
let weatherResponse = try JSONDecoder().decode(WeatherResponse.self, from: <YOUR API RESPONSE DATA>)
let weatherItems = weatherResponse.weatherItems
<DO WHATEVER YOU WANT WITH THE WEATHER ITEMS>
} catch let error {
print(error)
}

Swift ObjectMapper: How to parse array inside of an array

This is my JSON response:
[
[
{
"id": 22,
"request_id": "rqst5c12fc9e856ae1.06631647",
"business_name": "Code Viable",
"business_email": "code#viable.com",
"title": "Apache Load/Ubuntu",
}
],
[
{
"id": 24,
"request_id": "rqst5c130cae6f7609.41056231",
"business_name": "Code Viable",
"business_email": "code#viable.com",
"title": "Load",
}
]
]
This JSON structure got an array inside of an array, the object of the inner array is what I am trying to parse. Here is the my mapper:
struct JobResponseDataObject: Mappable {
init?(map: Map) {
}
var id: Int?
var requestId: String?
var businessName: String?
var businessEmail: String?
mutating func mapping(map: Map) {
id <- map["id"]
requestId <- map["request_id"]
businessName <- map["business_name"]
businessEmail <- map["business_email"]
}
}
I have tried create another mapper struct to hold the array of objects [JobResponseDataObject] and use Alamofire's responseArray with it, but it didn't work. I have also tried prefixing my json id with 0. but that didn't work too. Please help
Thank
So here's the deal...Codable is a pretty cool protocol from Apple to handle parsing JSON responses from APIs. What you're getting back is an array of arrays, so your stuff's gonna be look like this:
[[ResponseObject]]
So anyway, you'd make a struct of your object, like so:
struct ResponseObject: Codable {
let id: Int?
let requestId: String?
let businessName: String?
let businessEmail: String?
let title: String?
}
You'll note I changed the key name a bit (instead of request_id, I used requestId). The reason is JSONDecoder has a property called keyDecodingStrategy which presents an enum of canned decoding strategies you can select from. You'd do convertFromSnakeCase.
Here's code you can dump into a playground to tinker with. Basically, declare your struct, match it up to whatever the keys are in your JSON, declare a decoder, feed it a decoding strategy, and then decode it.
Here's how you could do an Alamofire call:
private let backgroundThread = DispatchQueue(label: "background",
qos: .userInitiated,
attributes: .concurrent,
autoreleaseFrequency: .inherit,
target: nil)
Alamofire.request(url).responseJSON(queue: backgroundThread) { (response) in
guard response.result.error == nil else {
print("💥KABOOM!💥")
return
}
if let data = response.data {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let parsedResponse = try decoder.decode([[ResponseObject]].self, from: data)
print(parsedResponse)
} catch {
print(error.localizedDescription)
}
}
}
Here's code you can chuck in a playground.
import UIKit
let json = """
[
[
{
"id": 22,
"request_id": "rqst5c12fc9e856ae1.06631647",
"business_name": "Code Viable",
"business_email": "code#viable.com",
"title": "Apache Load/Ubuntu",
}
],
[
{
"id": 24,
"request_id": "rqst5c130cae6f7609.41056231",
"business_name": "Code Viable",
"business_email": "code#viable.com",
"title": "Load",
}
]
]
"""
struct ResponseObject: Codable {
let id: Int?
let requestId: String?
let businessName: String?
let businessEmail: String?
let title: String?
}
if let data = json.data(using: .utf8) {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let parsedResponse = try decoder.decode([[ResponseObject]].self, from: data)
print(parsedResponse)
} catch {
print(error.localizedDescription)
}
}
You should use this JobResponseDataObject struct as [[JobResponseDataObject]] instead of [JobResponseDataObject] - where you are making a property using this struct in your parent struct or class.
You can use Codable here for mapping the JSON response, The JobResponseDataObject struct should look like,
struct JobResponseDataObject: Codable {
var id: Int?
var requestId: String?
var businessName: String?
var businessEmail: String?
var title: String?
private enum CodingKeys: String, CodingKey {
case id = "id"
case requestId = "request_id"
case businessName = "business_name"
case businessEmail = "business_email"
case title = "title"
}
}
let json = JSON(responseJSON: jsonData)
do {
if let value = try? json.rawData(){
let response = try! JSONDecoder().decode([[JobResponseDataObject]].self, from: value)
}
} catch {
print(error.localizedDescription)
}

Combine the results of 2 API calls fetching different properties for the same objects with RxSwift

I have a model called Track. It has a set of basic and a set of extended properties. List of tracks and their basic properties are fetched with a search API call, then I need to make another API call with those track IDs to fetch their extended properties.
The question is how to best combine the results of both API calls and populate the extended properties into the already created Track objects, and of course match them by ID (which unfortunately is a different property name in both calls' results). Note that there are many more keys returned in the real results sets - around 20-30 properties for each of the two calls.
Track.swift
struct Track: Decodable {
// MARK: - Basic properties
let id: Int
let title: String
// MARK: - Extended properties
let playbackURL: String
enum CodingKeys: String, CodingKey {
case id = "id"
case title = "title"
case playbackUrl = "playbackUrl"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let idString = try container.decode(String.self, forKey: CodingKeys.id)
id = idString.int ?? 0
title = try container.decode(String.self, forKey: CodingKeys.title)
playbackURL = try container.decodeIfPresent(String.self, forKey: CodingKeys.playbackUrl) ?? ""
}
}
ViewModel.swift
let disposeBag = DisposeBag()
var searchText = BehaviorRelay(value: "")
private let provider = MoyaProvider<MyAPI>()
let jsonResponseKeyPath = "results"
public lazy var data: Driver<[Track]> = getData()
private func searchTracks(query: String) -> Observable<[Track]> {
let decoder = JSONDecoder()
return provider.rx.request(.search(query: query))
.filterSuccessfulStatusCodes()
.map([Track].self, atKeyPath: jsonResponseKeyPath, using: decoder, failsOnEmptyData: false)
.asObservable()
}
private func getTracksMetadata(tracks: Array<Track>) -> Observable<[Track]> {
let trackIds: String = tracks.map( { $0.id.description } ).joined(separator: ",")
let decoder = JSONDecoder()
return provider.rx.request(.getTracksMetadata(trackIds: trackIds))
.filterSuccessfulStatusCodes()
.map({ result -> [Track] in
})
.asObservable()
}
private func getData() -> Driver<[Track]> {
return self.searchText.asObservable()
.throttle(0.3, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.flatMapLatest(searchTracks)
.flatMapLatest(getTracksMetadata)
.asDriver(onErrorJustReturn: [])
}
The JSON result for .search API call is structured like this:
{
"results": [
{
"id": "4912",
"trackid": 4912,
"artistid": 1,
"title": "Hello babe",
"artistname": "Some artist name",
"albumtitle": "The Best Of 1990-2000",
"duration": 113
},
{
...
}
]
}
The JSON result for .getTracksMetadata API call is structured like this:
[
{
"TrackID": "4912",
"Title": "Hello babe",
"Album": "The Best Of 1990-2000",
"Artists": [
{
"ArtistID": "1",
"ArtistName": "Some artist name"
}
],
"SomeOtherImportantMetadata1": "Something something 1",
"SomeOtherImportantMetadata2": "Something something 2",
"SomeOtherImportantMetadata3": "Something something 3"
},
{
...
}
]
The solution here is a two phase approach. First you should define two different structs for the two network calls and a third struct for the combined result. Let's say you go with:
struct TrackBasic {
let id: Int
let title: String
}
struct TrackMetadata {
let id: Int // or whatever it's called.
let playbackURL: String
}
struct Track {
let id: Int
let title: String
let playbackURL: String
}
And define your functions like so:
func searchTracks(query: String) -> Observable<[TrackBasic]>
func getTracksMetadata(tracks: [Int]) -> Observable<[TrackMetadata]>
Now you can make the two calls and wrap the data from the two separate endpoints into the combined struct:
searchText
.flatMapLatest { searchTracks(query: $0) }
.flatMapLatest { basicTracks in
Observable.combineLatest(Observable.just(basicTracks), getTracksMetadata(tracks: basicTracks.map { $0.id }))
}
.map { zip($0.0, $0.1) }
.map { $0.map { Track(id: $0.0.id, title: $0.0.title, playbackURL: $0.1.playbackURL) } }
The above assumes that the track metadata comes in the same order that it was requested in. If that is not the case then the last map will have to be more complex.

Multiple codable object types out of a single JSON return

My dilemma is I'm receiving two different object types from one table in a JSON response. Here is an example of the response of both types in a return.
"supplementaryItems": [
{
"header": "Doodle",
"subHeader": "It's a drawing.",
"slideID": 4,
"imageName": null,
"textItems": null,
"sortOrder": 0
},
{
"header": "Cell Phones",
"subHeader": "No phones please",
"slideID": 8,
"imageName": "welcome_icon_cellphones",
"textItems": ["first","second","third"],
"sortOrder": 1
}
]
What we're hoping to do is create two different types of objects here. A textOnlyItem, and a imageWithTextItem.
Is there a way to create one as a subclass or extension that can be keyed off of a Bool defined by whether imageName is null or not?
Thanks for any help all.
You don't need two different objects. Just declare imageName and textItems as optional, this handles the null case.
You can simply check whether imageName is nil
let jsonString = """
{"supplementaryItems": [
{
"header": "Doodle",
"subHeader": "It's a drawing.",
"slideID": 4,
"imageName": null,
"textItems": null,
"sortOrder": 0
},
{
"header": "Cell Phones",
"subHeader": "No phones please",
"slideID": 8,
"imageName": "welcome_icon_cellphones",
"textItems": ["first","second","third"],
"sortOrder": 1
}
]
}
"""
struct Root : Decodable {
let supplementaryItems : [SupplementaryItem]
}
struct SupplementaryItem : Decodable {
let header : String
let subHeader : String
let slideID : Int
let imageName : String?
let textItems : [String]?
let sortOrder : Int
}
do {
let data = Data(jsonString.utf8)
let result = try JSONDecoder().decode(Root.self, from: data)
for item in result.supplementaryItems {
if let imageName = item.imageName {
print(imageName + " has text items")
} else {
print(item.header + " has no text items")
}
}
} catch { print(error) }
I actually like vadian's approach, of one type. But I gather that would require significant refactoring in your situation.
The other approach is to just use JSONSerialization and build your heterogeneous array manually. JSONSerialization isn't deprecated, it just doesn't do it automatically like JSONDecoder.
Another approach is to use JSONDecoder, writing custom initializer that tries to decode it as ImageItem, and if that fails, try decoding it as TextItem:
protocol SupplementaryItem {
var header: String { get }
var subHeader: String { get }
var slideID: Int { get }
var sortOrder: Int { get }
var textItems: [String]? { get }
}
struct TextItem: SupplementaryItem, Codable {
let header: String
let subHeader: String
let slideID: Int
let sortOrder: Int
let textItems: [String]?
}
struct ImageItem: SupplementaryItem, Codable {
let header: String
let subHeader: String
let slideID: Int
let sortOrder: Int
let textItems: [String]?
let imageName: String
}
struct ResponseObject: Decodable {
let supplementaryItems: [SupplementaryItem]
enum CodingKeys: String, CodingKey {
case supplementaryItems
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
var container = try values.nestedUnkeyedContainer(forKey: .supplementaryItems)
if container.count == nil {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Expected array for supplementaryItems")
}
var items = [SupplementaryItem]()
while !container.isAtEnd {
if let item = try? container.decodeIfPresent(ImageItem.self), let imageItem = item {
items.append(imageItem)
} else {
let textItem = try container.decode(TextItem.self)
items.append(textItem)
}
}
supplementaryItems = items
}
}
Then:
let string = """
{
"supplementaryItems": [
{
"header": "Doodle",
"subHeader": "It's a drawing.",
"slideID": 4,
"imageName": "foo",
"textItems": null,
"sortOrder": 0
},
{
"header": "Cell Phones",
"subHeader": "No phones please",
"slideID": 8,
"imageName": "welcome_icon_cellphones",
"textItems": ["first","second","third"],
"sortOrder": 1
}
]
}
"""
let data = string.data(using: .utf8)!
let json = try! JSONDecoder().decode(ResponseObject.self, from: data)
print(json)
I'm not convinced this is any better or worse than just using JSONSerialization, but it's another approach.

Resources