How to retrieve values from a Nested JSON Swift - ios

So I've been working with a nested JSON file (that I added locally to my project) in Swift. I've included a part of the JSON file I'm working on below. The data is structured as follows:
{
"categories": [
{
"categoryName": "Albatrosses",
"exercisesInCategory": [
"Wandering albatross",
"Grey-headed albatross",
"Black-browed albatross",
"Sooty albatross",
"Light-mantled albatross"
]
},
{
"categoryName": "Cormorants",
"exercisesInCategory": [
"Antarctic shag",
"Imperial shag",
"Crozet shag"
]
},
{
"categoryName": "Diving petrels",
"exercisesInCategory": [
"South Georgia diving petrel",
"Common diving petrel"
]
},
{
"categoryName": "Ducks, geese and swans",
"exercisesInCategory": [
"Yellow-billed pintail"
]
}
]
}
In order to retrieve the data I made 2 structures that represent the data in the JSON so I can then retrieve values from it. These are as follows:
struct Response:Codable{
let categories: [Categories]
}
struct Categories:Codable{
let categoryName : String?
let exercisesInCategory : [String]
}
The file name is fitnessData.json and I'm trying to retrieve the data from it by using this code:
private func parse(){
print("Retrieving JSON Data...")
if let url = Bundle.main.url(forResource: "fitnessData", withExtension: "json") {
do {
let data = try Data(contentsOf: url)
self.response = try JSONDecoder().decode(Response.self, from: data)
if let responseJSON = self.response {
print("The categories are: ", responseJSON.categories[1].categoryName!)
}
} catch {
print(error)
}
}
}
The problem is that I would like to retrieve ALL the 'categoryName' values from the JSON file, and ALL the 'exercisesInCategory' values. But so far I've only managed to navigate towards a specific item in the JSON file and retrieving that item i.e.
responseJSON.categories[1].categoryName!
I would like to iterate over the JSON file to get all of the 'categoryName' values for example. However in order to do that I'd have to write something like this:
for value in responseJSON.categories[1].categoryName! {
print(value)
}
Where '1' represents all the values for the categories struct. The code above will obviously print only the categoryName of the second index in the categories array in the JSON file. Could someone point me in the right direction?

You can do that like:
for category in responseJSON.categories {
print(category.categoryName!)
}
Or you can use map function for getting all the categoryName like:
let categoryNames = responseJSON.categories.map {$0.categoryName}

Simply like this.
response.categories.forEach { print($0.categoryName) }

If you would like to put both values in different arrays:
var categoryNameList = [String]
var excercisesInCategory = [[String]]
for category in responseJSON.categories {
categoryNameList.append(category.categoryName)
excercisesInCategory.append(category. exercisesInCategory)
}

this.
let categories = responseJSON.categories.map { $0.categoryName }
categories.forEach { print($0) }

If you iterate through the String, each item is single character of string.
You can iterate through categories array
for category in responseJSON.categories {
print(category.categoryName ?? "No name")
}
To get all names, you can use compactMap which removes nil values from an array
let names = responseJSON.categories.compactMap { $0.categoryName }
Next:
If each category has its name, make type of this property non-optional String (then you can use map instead of compactMap)
I would improve your naming. Rename categoryName key of json to name using CodingKeys enum. Then you could do
category.name

If you wanted to get all the categoryName and exercisesInCategory form JSON file, then you don't need to pass hard coded index. Use the following modified function, it will do the work..
private func parse(){
print("Retrieving JSON Data...")
if let url = Bundle.main.url(forResource: "fitnessData", withExtension: "json") {
do {
let data = try Data(contentsOf: url)
let response = try JSONDecoder().decode(Response.self, from: data)
for category in response.categories {
print("Category Name: \(category.categoryName!)")
for exercises in category.exercisesInCategory {
print("Exercise in category: \(exercises)")
}
}
} catch {
print(error)
}
}
}

Related

Xcode, The given data was not valid JSON, Can't read the JSON from API properly

Hello I am creating an app with Xcode and I am having the following problem, I created this API (if you enter the link you'll see the JSON data) https://proyecto-idts6.epizy.com/models/getCategorias.php
If you dont want to enter the link here is how this si how the structure of the JSON looks like:
{
"items":[
{
"categorie":"Fruits",
"id_categorie":"1"
},
{
"categorie":"Animals",
"id_categorie":"2"
},
{
"categorie":"Juices",
"id_categorie":"3"
},
{
"categorie":"Vegetables",
"id_categorie":"4"
},
{
"categorie":"Alcohol",
"id_categorie":"5"
},
{
"categorie":"Desserts",
"id_categorie":"6"
}
]
}
The problem I have is that when I try to decode the data from the API it cant't be decoded properly, I am trying to recreate the same code of this youtube video, but with my API: https://www.youtube.com/watch?v=sqo844saoC4
What I want basically is to print the categories and storage each of them in variables (because i'll need to move the variables between screens)
This is how my code looks like:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let url = "https://proyecto-idts6.epizy.com/models/getCategorias.php"
getData(from: url)
//Here is where i want to storage the variables from the JSON
}
private func getData(from url: String) {
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { data, response, error in
guard let data = data, error == nil else {
print("something went wrong")
return
}
do {
let result = try JSONDecoder().decode([ResultItem].self, from: data)
print(result)
}
catch {
print("failed to convert\(error)")
}
})
task.resume()
}
}
struct Response: Codable {
let items: [ResultItem]
}
struct ResultItem: Codable {
let categorie: String
}
My goal is to have variables for example like this: categorie1=("the category 1 called from the JSON"), categorie2=("the category 2 called from the JSON"), categorie3=("the category 3 called from the JSON"),...
The problem is not in the decoding but in the remote API.
Your endpoint (https://proyecto-idts6.epizy.com/models/getCategorias.php) instead of returning a JSON is returning the following HTML
<html><body><script type="text/javascript" src="/aes.js" ></script><script>function toNumbers(d){var e=[];d.replace(/(..)/g,function(d){e.push(parseInt(d,16))});return e}function toHex(){for(var d=[],d=1==arguments.length&&arguments[0].constructor==Array?arguments[0]:arguments,e="",f=0;f<d.length;f++)e+=(16>d[f]?"0":"")+d[f].toString(16);return e.toLowerCase()}var a=toNumbers("f655ba9d09a112d4968c63579db590b4"),b=toNumbers("98344c2eee86c3994890592585b49f80"),c=toNumbers("f5490e280a5e50f74932909856c3d3a3");document.cookie="__test="+toHex(slowAES.decrypt(c,2,a,b))+"; expires=Thu, 31-Dec-37 23:55:55 GMT; path=/"; location.href="https://proyecto-idts6.epizy.com/models/getCategorias.php?i=1";</script><noscript>This site requires Javascript to work, please enable Javascript in your browser or use a browser with Javascript support</noscript></body></html>
So you are trying to decode that HTML content, which clearly leads to the error your reported
failed to convertdataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: Optional(Error Domain=NSCocoaErrorDomain Code=3840 "Invalid value around line 1, column 0." UserInfo={NSDebugDescription=Invalid value around line 1, column 0., NSJSONSerializationErrorIndex=0})))

Populate model with API result

I am looking to populate my model with the "payload" section from my endpoint. I have created a model of DataResponse which has a record property of Payload. I would like to get only the data from the payload section of the API endpoint. My network call is incorrect and I must be structuring my models wrong, but I am not sure what needs to be fixed. I am not sure if it makes a difference but my endpoint was displaying as an XML and I converted it to JSON below.
struct DataResponse: Decodable {
let record: Payload
}
struct Payload: Decodable {
let SoldToday: Int
}
let url = URL(string: "https:------")!
URLSession.shared.dataTask(with: url) {data, response, error in
guard error == nil,
let data = data else {
print(error)
return
}
let dataResponse = try? JSONDecoder().decode(DataResponse.self, from: data)
if let dataResponse = dataResponse {
print(dataResponse.record.SoldToday)
}
}.resume()
These are the contents of my url endpoint:
{
"action": "API_DoQuery",
"errcode": "0",
"errtext": "No error",
"dbinfo": {
"name": "Daily",
"desc": []
},
"variables": {
"__iol": "&rand='+new Date().getTime())};\">",
"__script": "&rand='+new Date().getTime());void(0);",
"iol": "<img qbu='module' src='/i/clear2x2.gif' onload=\"javascript:if(typeof QBU=='undefined'){QBU={};$.getScript(gReqAppDBID+'?a=dbpage&pagename=",
"script": "javascript:$.getScript(gReqAppDBID+'?a=dbpage&pagename="
},
"chdbids": [],
"record": {
"payload": "{ \"RecordID\": 04-22-2022, \"SoldToday\": 18, \"ContractToday\": 869327, \"KWToday\": 160960 }",
"update_id": "1647544685640"
}
}
you need to fix 2 things to be able to decode your json data:
You need the models that match your json data. Such as:
struct DataResponse: Decodable {
let record: Record
}
struct Record: Decodable {
let payload: Payload
}
struct Payload: Decodable {
let SoldToday: Int
}
And you need to make sure your data is valid json. Currently variables is not valid, similarly for payload in record,
(it is enclosed in quotes). Once these are fixed, I was able to decode the data successfully in my tests.
Note that if your endpoint is giving you XML, then it is probably better to convert XML to your models directly, without having to convert to json. There are a number of XML parser libraries on github.

Is it possible to create Swift Codable for plain k-v json?

I've JSON data like:
{
"peopleA": "nnll",
"peopleB": "ihyt",
"peopleC": "udr",
"peopleD": "vhgd",
"peopleE": "llll"
}
There're thousands of data like that, basically what I wanna to do is read the JSON file, and fetch the relate info, like: input peopleC, return udr.
Trying to use some online solution, I got
struct Welcome: Codable {
let peopleA, peopleB, peopleC, peopleD: String
let peopleE: String
}
I know I can refactor the JSON file to:
{
"candidates": [
{
"name": "peopleA",
"info": "nnll"
},
{
"name": "peopleB",
"info": "ihyt"
},
{
"name": "peopleC",
"info": "udr"
}
]
}
And get the related Swift struct:
struct Welcome: Codable {
let candidates: [Candidate]
}
// MARK: - Candidate
struct Candidate: Codable {
let name, info: String
}
I'm just wondering if maybe we could make it work in Swift without postprocessing the json file?
You can simply decode it as a dictionary. Then you can map your dictionary into your array of Candidate structures if you would like to:
struct Welcome: Codable {
let candidates: [Candidate]
}
struct Candidate: Codable {
let name, info: String
}
let js = """
{
"peopleA": "nnll",
"peopleB": "ihyt",
"peopleC": "udr",
"peopleD": "vhgd",
"peopleE": "llll"
}
"""
do {
let dictionary = try JSONDecoder().decode([String: String].self, from: Data(js.utf8))
let welcome = Welcome(candidates: dictionary.map(Candidate.init))
print(welcome)
} catch {
print(error)
}
This will print:
Welcome(candidates: [Candidate(name: "peopleA", info: "nnll"), Candidate(name: "peopleC", info: "udr"), Candidate(name: "peopleB", info: "ihyt"), Candidate(name: "peopleE", info: "llll"), Candidate(name: "peopleD", info: "vhgd")])

How to parse a optional JSON object using JSONJoy?

https://github.com/daltoniam/JSONJoy-Swift
For example :
JSON1 = {
"message": "Sorry! Password does not match.",
"code": "4"
}
JOSN2 = {
"data": {
"id": 21
},
"message": "Signup Successful.",
"code": "1"
},
Here json key “data” is optional. Then how I can handle both response using the same model object??
JSONJoy natively sets not found elements to nil, you just have to declare them optional and then check for nil before using them.
From the docs
This also has automatic optional validation like most Swift JSON libraries.
//some randomly incorrect key. This will work fine and the property
will just be nil.
firstName = decoder[5]["wrongKey"]["MoreWrong"].string
//firstName is nil, but no crashing!
Here is my example that my be illustrative. I have a complex object set where my top level object (UserPrefs) has arrays of secondary objects (SmartNetworkNotification and SmartNotificationTime).
Note that notifications and times are both declared as optional. What I do is check for nil after attempting to parse the secondary object arrays. Without the nil check the attempt to iterate on the parsed list fails since its nil. With the nil check it just moves past it if its empty.
This works for me but isn't deeply tested yet. YMMV! Curious how others are handling it.
struct UserPrefs: JSONJoy {
var notifications: [SmartNetworkNotification]?
var times: [SmartNotificationTime]?
init(_ decoder: JSONDecoder) throws {
// Extract notifications
let notificationsJson = try decoder["notifications"].array
if(notificationsJson != nil){
var collectNotifications = [SmartNetworkNotification]()
for notificationDecoder in notificationsJson! {
do {
try collectNotifications.append(SmartNetworkNotification(notificationDecoder))
} catch let error {
print("Error.. on notifications decoder")
print(error)
}
}
notifications = collectNotifications
}
// Extract time of day settings
let timesJson = try decoder["times"].array
if(timesJson != nil){
var collectTimes = [SmartNotificationTime]()
for timesDecoder in timesJson! {
do {
try collectTimes.append(SmartNotificationTime(timesDecoder))
} catch let error {
print("Error.. on timesJson decoder")
print(error)
}
}
times = collectTimes
}
}

How can I use swiftyJSON dictionaryValue as a usable string for a UILabel?

I have a makeRequest() method inside a UITableViewController with the following code:
func makeRequest() {
Alamofire.request(.GET, self.foursquareEndpointURL, parameters: [
//"VENUE_ID" : self.foursquareVenueID,
"client_id" : self.foursquareClientID,
"client_secret" : self.foursquareClientSecret,
"v" : "20140806"
])
.responseJSON(options: nil) { (_, _, data, error) -> Void in
if error != nil {
println(error?.localizedDescription)
} else if let data: AnyObject = data {
let jObj = JSON(data)
if let venue = jObj["response"]["venue"].dictionaryValue as [String: JSON]? {
self.responseitems = jObj
println("venue is: \(venue)")
}
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData() // Update UI
}
}
}
}
also keep in mind that I have a property var responseitems:JSON = []
println("venue is: \(venue)") prints a nice looking response to console, so I know that is working correctly...
I also have a custom UITableViewCell class with a bindData() method with the following code:
func bindData() {
println("VenueDetailHeaderCell data did set")
self.venueDetailTitleLabel.text = self.headerInfo?["name"].stringValue
let labelData = self.headerInfo?["name"].stringValue
println("labelData is: \(labelData)")
}
As you can see, I am attempting to set a UILabel's text to the ["name"].stringValue in the JSON response. However, when I println("labelData is: \(labelData)") I get console output of labelData is: Optional("") which is obviously empty.
Here's a screenshot of what I'm trying to grab
What am I doing wrong here and how can I grab the name of the venue and assign my UILabel to it?
UPDATE:
I tried the following code
let labelData = self.headerInfo?["name"].error
println("labelData is: \(labelData)")
And get a console output of: "Error Domain=SwiftyJSONErrorDomain Code=901 "Array[0] failure, It is not an array" UserInfo=0x7fd6d9f7dc10 {NSLocalizedDescription=Array[0] failure, It is not an array}" If that is of use to anyone. I am really confused here... Any ideas?
The problem is that the headerInfo value is an error JSON object, because you're trying to access a dictionary with an integer index.
Note that var responseitems:JSON = [] does not create an array object. SwiftyJSON has auto-assignment-constructors (I'm new to swift, so not sure what the correct swift terminology is)... see this initialiser in the SwiftyJSON.swift source code:
extension JSON: ArrayLiteralConvertible {
public init(arrayLiteral elements: AnyObject...) {
self.init(elements)
}
}
What this means is that when you do var responseitems:JSON = [] you are not creating an array, you are creating a JSON object that is constructed with an empty array using the above init method. Then when you do self.responseitems = jObj you are re-assigning that responseitems variable to a JSON object with a dictionary in it. Therefore self.responseitems[0] is invalid.
Also note that with SwiftyJSON, there is no such thing as an optional JSON object. I notice in your comment you say that you do var headerInfo:JSON? ... - it's not possible to have an optional JSON.
var headerInfo: JSON = nil
The above is possible - this uses another auto-initialiser that initialises a valid JSON object that represents the JSON null value.
So, how to fix it?
When you assign headerInfo do it like this:
let headerInfo = self.responseitems["response"]["venue"]
And now in bindData you can do:
self.venueDetailTitleLabel.text = self.headerInfo["name"].stringValue
Note that all of the above assumes Swift 1.2 and SwiftyJSON >= 2.2 - also after you've understood the above and corrected the issue, you will probably want to refactor the code a bit to reflect the corrected understanding of the data-model.
The way to parse is the next
element: JSONValue //Something with JSONValue
if let title = element["name"]?.string {
cell.title.text = title
}

Resources