parsing json xcode swift 4 - json

Problem. I want to parse the json from here and show on a table view, i am using swift 4 and Decodable. but i am getting a type mismatch error. Link to json:
https://newsapi.org/v2/top-headlines?sources=techcrunch&apiKey=50fb91212a47432d802b3b1ac0f717a3
My Struct looks like this.
struct Root : Decodable {
let status : String
// let totalResults : Int
let articles : [Article]
}
struct Article : Decodable {
let source: Source
let author, title, description: String
let url: URL
// let publishedAt: Date
let urlToImage: String
}
struct Source: Decodable {
let id, name: String
}
My ViewDidLoad Looks like this :
var articles : [Article]? = []
override func viewDidLoad() {
super.viewDidLoad()
tableview.delegate = self
tableview.dataSource = self
fetchArticles()
}
func fetchArticles(){
let jsonURLString = "https://newsapi.org/v2/top-headlines?sources=techcrunch&apiKey=50fb91212a47432d802b3b1ac0f717a3"
guard let url = URL(string: jsonURLString) else { return }
URLSession.shared.dataTask(with: url) { (data,response,err) in
guard let data = data else { return }
self.myArticles = [Article]()
do{
let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64
let root = try decoder.decode(Root.self, from: data)
self.myArticles = root.articles
DispatchQueue.main.async {
self.tableview.reloadData()
}
} catch let error{
print(error)
}
}.resume()
}
My cellForRowAtIndexPath
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let myCell = tableView.dequeueReusableCell(withIdentifier: "articleCell", for: indexPath) as! ArticleCell
let article = self.myArticles[indexPath.row]
myCell.title.text = article.title
myCell.body.text = article.description
myCell.author.text = article.author
myCell.imgView.downloadImage(from: ("https://tctechcrunch2011.files.wordpress.com/2017/04/uber-vs-waymo.png"))
return myCell
}
Error i am getting.
No errors, nothing loads to the table view.

The error is clear. You are going to decode an array but the object is a dictionary.
This decodes the JSON including the special decoding to URL and Date.
The root object is a dictionary, the key articles contains the articles and source is a dictionary, not an array
struct Root : Decodable {
let status : String
let articles : [Article]
}
struct Article : Decodable {
let source: Source
let author, title, description: String
let url: URL
let publishedAt: Date
let urlToImage: String
}
struct Source: Decodable {
let id, name: String
}
var articles = [Article]()
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let root = try JSONDecoder().decode(Root.self, from: data)
self.articles = root.articles
DispatchQueue.main.async {
self.tableview.reloadData()
}
} catch { print(error) }
There is no need to use classes inheriting from NSObject
And do not declare the data source object as optional and get the same item a couple of times in cellForRow and it's indexPath.row
...
let article = self.articles[indexPath.row]
myCell.title.text = article.title
myCell.body.text = article.description
myCell.author.text = article.author
...

Related

Swift parsing JSON into Table View doesnt work

Hi I have problems to read the Json Data into the TableView.
Can anybody help me, its my first time working with it.
I know I am doing something wrong, but I cant finde the Solution for my JSON File cause in the Internet they use simple ones..
Here is my JSON:
{
"data":[
{
"title": "Brot",
"desc":[
{
"Name": "Roggenschrot- und Roggenvollkornbrot",
"Menge": "Gramm",
"Kalorie": "2",
"Energiedichte": "Gelb"
},
{
"Name": "Weizenschrot- und Weizenvollkornbrot",
"Menge": "Gramm",
"Kalorie": "2",
"Energiedichte": "Gelb"
},
{
"Name": "Weizenschrot- und Weizenvollkornbrot",
"Menge": "Gramm",
"Kalorie": "2",
"Energiedichte": "Gelb"
},
]
}
]
}
Here is my tableView
import UIKit
class EseenTagebuchTableViewController: UITableViewController {
var result: Result?
var resultItem: ResultItem?
override func viewDidLoad() {
super.viewDidLoad()
parseJSON()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return result?.data.count ?? 0
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return result?.data[section].title
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
if let result = result {
return result.data.count
}
return 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let text = resultItem?.desc?[indexPath.section].Name?[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = text
return cell
}
private func parseJSON() {
guard let path = Bundle.main.path(forResource: "Ernahrungstagebuch", ofType: "json") else {
return
}
let url = URL(fileURLWithPath: path)
do {
let jsonData = try Data(contentsOf: url)
result = try JSONDecoder().decode(Result.self, from: jsonData)
return
}
catch {
print("Error: \(error)")
}
}
}
And here are my Model 1 and after my Model 2
import Foundation
struct Result: Codable {
let data: [ResultItem]
}
struct ResultItem: Codable {
let title: String
let desc: [descItems]?
}
Model 2
import Foundation
struct descItems: Codable {
let Name: String?
let Menge: String?
let Kalorie: Int?
let Energiedichte: String?
}
What am I doing wrong?
First your json data in your file is not correct, there is an extra "," comma after
the last "Energiedichte": "Gelb" , remove that from your file.
Second your model is not correct, you should have:
struct Result: Codable {
let data: [ResultItem]
}
struct ResultItem: Codable {
let title: String
let desc: [DescItems]?
}
struct DescItems: Codable {
let Name: String?
let Menge: String?
let Kalorie: String? // <-- here not Int?
let Energiedichte: String?
}
Using your code parseJSON() I was able to parse the data without errors.
EDIT-1: if you really want kalories to be Int, try:
struct DescItems: Codable {
let name: String?
let menge: String?
let kalorie: Int? // <-- here
let energiedichte: String?
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
menge = try container.decode(String.self, forKey: .menge)
energiedichte = try container.decode(String.self, forKey: .energiedichte)
let calorie = try container.decode(String.self, forKey: .kalorie) // <-- here
kalorie = Int(calorie) // <-- here
}
enum CodingKeys: String, CodingKey {
case name = "Name"
case menge = "Menge"
case kalorie = "Kalorie"
case energiedichte = "Energiedichte"
}
}
Note the change of case, as per common practice.
you can also use json parse below code:
func parseJSON() {
let path = Bundle.main.path(forResource: "Ernahrungstagebuch", ofType: "json")
let jsonData = try? NSData(contentsOfFile: path!, options: NSData.ReadingOptions.mappedIfSafe)
let jsonResult: NSDictionary = try! (JSONSerialization.jsonObject(with: jsonData! as Data, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary)!
if let data = jsonResult["data"] as? [[String:Any]] {
self.arrPlan = data.map{Day(JSON: $0)!}
// print(arrPlan)
}
}
Try to this code for get data array object and after reload tableview Data.
You can use this extension to parse json.
import Foundation
extension Bundle {
func decode<T: Decodable>(_ type: T.Type, from file: String, dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate, keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys) -> T {
guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Failed to locate \(file) in bundle.")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("Failed to load \(file) from bundle.")
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = dateDecodingStrategy
decoder.keyDecodingStrategy = keyDecodingStrategy
do {
return try decoder.decode(T.self, from: data)
} catch DecodingError.keyNotFound(let key, let context) {
fatalError("Failed to decode \(file) from bundle due to missing key '\(key.stringValue)' not found – \(context.debugDescription)")
} catch DecodingError.typeMismatch(_, let context) {
fatalError("Failed to decode \(file) from bundle due to type mismatch – \(context.debugDescription)")
} catch DecodingError.valueNotFound(let type, let context) {
fatalError("Failed to decode \(file) from bundle due to missing \(type) value – \(context.debugDescription)")
} catch DecodingError.dataCorrupted(_) {
fatalError("Failed to decode \(file) from bundle because it appears to be invalid JSON")
} catch {
fatalError("Failed to decode \(file) from bundle: \(error.localizedDescription)")
}
}
}
And use it like this:
let incomingData = Bundle.main.decode([Your_Model].self, from: "Your_Json_File_name.json")

Cannot assign value of type 'MRData' to type '[F1Data]' when trying to parse JSON

I have been wrestling with this for a while. I am trying to parse a JSON Api into a UITableview. The url is Formula One API . I am using Codable rather than third party pods. Thought this might cut down on the amount of code. Although, as the API is not that straight forward it is hard to extract what I want. Basically, I want to list the current standing for drivers for a given year. In url and code I have given I have chosen 1999 as an example. I have been researching on Stackoverflow but each solution is very specific to a particular problem and I can't seem to relate to my issue. Below is the code I have.
struct MRData: Codable {
let xmlns: String?
let series: String?
let url: String?
let limit, offset, total: String?
let standingsTable: StandingsTable
enum CodingKeys: String, CodingKey {
case xmlns, series, url, limit, offset, total
case standingsTable = "StandingsTable"
}
}
struct StandingsTable: Codable {
let season: String?
let standingsLists: [StandingsList]
enum CodingKeys: String, CodingKey {
case season
case standingsLists = "StandingsLists"
}
}
struct StandingsList: Codable {
let season, round: String?
let driverStandings: [DriverStanding]
enum CodingKeys: String, CodingKey {
case season, round
case driverStandings = "DriverStandings"
}
}
struct DriverStanding: Codable {
let position, positionText, points, wins: String?
let driver: Driver
let constructors: [Constructor]
enum CodingKeys: String, CodingKey {
case position, positionText, points, wins
case driver = "Driver"
case constructors = "Constructors"
}
}
struct Constructor: Codable {
let constructorId: String?
let url: String?
let name: String?
let nationality: String?
}
struct Driver: Codable {
let driverId: String?
let url: String?
let givenName, familyName, dateOfBirth, nationality: String?
}
class f1TableViewController: UITableViewController {
var champions: [F1Data] = []
override func viewDidLoad() {
super.viewDidLoad()
// let jsonUrlString = "https://api.letsbuildthatapp.com/jsondecodable/website_description"
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.title = "Champion Drivers"
fetchJSON()
}
private func fetchJSON(){
let jsonUrlString = "https://ergast.com/api/f1/1999/driverstandings.json"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
DispatchQueue.main.async {
if let err = err {
print("Failed to get data from url:", err)
return
}
guard let data = data else { return }
do {
let decoder = JSONDecoder()
// Swift 4.1
decoder.keyDecodingStrategy = .convertFromSnakeCase
self.champions = try decoder.decode(MRData.self, from: data)
self.tableView.reloadData()
//let season = f1Data.mrData.standingsTable.season
// let firstDriver = f1Data.mrData.standingsTable.standingsLists[0].driverStandings
// for driver in firstDriver {
//
// print("\(driver.driver.givenName) \(driver.driver.familyName)")
// }
//print(season)
} catch {
print(error)
}
}
}.resume()
}
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return champions.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "cellId")
let champion = champions[indexPath.row]
let driverName = champion.mrData.standingsTable.standingsLists[0].driverStandings
for driver in driverName {
cell.textLabel?.text = driver.driver.familyName
}
//cell.textLabel?.text =
//cell.detailTextLabel?.text = String(course.numberOfLessons)
return cell
}
}
Now I realize that the error is in the do catch block.
do {
let decoder = JSONDecoder()
// Swift 4.1
decoder.keyDecodingStrategy = .convertFromSnakeCase
self.champions = try decoder.decode(MRData.self, from: data)
self.tableView.reloadData()
and that the array F1Data cannot be a dictionary of MRData. So if I change it to the following self.champions = try decoder.decode([F1Data].self, from: data) the I get another error which is
debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil)). Any help would be appreciated with this.
As Vadian correctly said that you need to decode at the root of your object. I briefly watched this video and the penny dropped! Assign the decoder to a variable and add to decode the complete structure starting at the root object.
guard let data = data else { return }
do {
let decoder = JSONDecoder()
// Swift 4.1
decoder.keyDecodingStrategy = .convertFromSnakeCase
let firstDriver = try decoder.decode(F1Data.self, from: data)
self.champions = firstDriver.mrData.standingsTable.standingsLists[0].driverStandings
self.tableView.reloadData()

Allow JSON fragments with Decodable

import UIKit
class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
//#IBOutlet weak var ingredientText: UILabel!
struct Recipes: Decodable {
let recipe_id:String?
let image_url:String?
let source_url:String?
let f2f_url:String?
let title:String?
let publisher:String?
let social_rank:Float64?
let page:Int?
let ingredients:[String]?
private enum CodingKeys: String, CodingKey{
case recipe_id = "recipe_id"
case image_url = "image_url"
case source_url = "source_url"
case f2f_url = "f2f_url"
case title = "title"
case publisher = "publisher"
case social_rank = "social_rank"
case page = "page"
case ingredients = "ingredients"
}
}
var recipes = [Recipes]()
var food = "chicken"
var food2 = "peas"
var food3 = "onions"
//var recipeData = [Recipe]
#IBOutlet weak var tableView: UITableView!
fileprivate func getRecipes() {
let jsonURL = "http://food2fork.com/api/search?key=264045e3ff7b84ee346eb20e1642d9d9264045e3ff7b84ee346eb20e1642d9d9&food=chicken&food2=onions&food3=peas"
guard let url = URL(string: jsonURL) else{return}
URLSession.shared.dataTask(with: url) {(data, _ , err) in
DispatchQueue.main.async {
if let err = err{
print("failed to get data from URL",err)
return
}
guard let data = data else{return}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
self.recipes = try decoder.decode([Recipes].self, from: data)
self.tableView.reloadData()
}catch let jsonERR {
print("Failed to decode",jsonERR)
}
}
}.resume()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return recipes.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "cell")
let recipe = recipes[indexPath.row]
cell.textLabel?.text = recipe.title
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.title = "Ingredients"
getRecipes()
}
}
I am getting the error:
JSON text did not start with array or object and option to allow fragments not set." UserInfo={NSDebugDescription=JSON text did not start with array or object and option to allow fragments not set.})))
JSONDecoder doesn't provide any JSONSerialization.ReadingOptions.
You could make a manual check whether the first byte of the data is an opening square bracket <5b> or brace <7b>
guard let data = data, let firstByte = data.first else { return }
guard firstByte == 0x5b || firstByte == 0x7b else {
let string = String(data: data, encoding: .utf8)!
print(string)
return
}
However I'd recommend to use the response parameter to check for status code 200
URLSession.shared.dataTask(with: url) { (data, response , error) in
if let response = response as? HTTPURLResponse, response.statusCode != 200 {
print(response.statusCode)
return
}
...
Note: If the CodingKeys match exactly the struct members you can omit the CodingKeys and as you are explicitly using .convertFromSnakeCase you are encouraged to name the struct members recipeId, imageUrl, sourceUrl etc.
You want to decode [Recipe], that is, an Array of Recipe. That mean the first (non-whitespace) character in data has to be [ (to make it a JSON array), and it's not. So you need to figure out why you're getting the wrong response, and fix that problem. Try converting data to a String and printing it:
print(String(data: data, encoding: .utf8))

Reading local JSON file and using it to populate UITableView

I am writing an app that needs to look at a local JSON file, then compare it's version to one I have hosted on a website. If they don't match, download the one from the web and save it locally. If they do match, then continue on and use the local JSON file. This version info is in the JSON file itself.
Previously, my app would simply parse the online data and use that directly. It would then populate the UITableView using the JSON data. Now that I am using my local file, the UITableView isn't getting populating, and I'm not certain how to fix it. From reading the new function, I think my issue is that I'm not using JSONDecoder(), and instead using JSONSerialization(), and therefore I can't point it at the specific metadata I want.
26 Jun 18 Edit (Below is my BonusListViewController.swift file):
//
// BonusListViewController.swift
// Tour of Honor
//
// Created by Tommy Craft on 6/6/18.
// Copyright © 2018 Tommy Craft. All rights reserved.
//
import UIKit
import os.log
import Foundation
class BonusListViewController: UITableViewController {
var bonuses = [JsonFile.JsonBonuses]()
let defaults = UserDefaults.standard
override func viewDidLoad() {
super.viewDidLoad()
// MARK: Data Structures
// Settings Struct
struct Constants {
struct RiderData {
let riderNumToH = "riderNumToH"
let pillionNumToH = "pillionNumToH"
}
struct RallyData {
let emailDestinationToH = "emailDestinationToH"
}
}
//MARK: Check for updated JSON file
checkJSON()
//MARK: Trigger JSON Download
/*
downloadJSON {
print("downloadJSON Method Called")
}
*/
}
// MARK: - Table View Configuration
// MARK: Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
print("Found \(bonuses.count) sections.")
return bonuses.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("Found \(bonuses.count) rows in section.")
return bonuses.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
cell.textLabel?.text = bonuses[indexPath.section].name.capitalized
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "showDetail", sender: self)
}
// MARK: - Table View Header
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 30
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return bonuses[section].state
}
override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 3
}
// MARK: Functions
// MARK: - Download JSON from ToH webserver
func downloadJSON(completed: #escaping () -> ()) {
let url = URL(string: "http://tourofhonor.com/BonusData.json")
URLSession.shared.dataTask(with: url!) { [weak self] (data, response, error) in
if error == nil {
do {
let posts = try JSONDecoder().decode(JsonFile.self, from: data!)
DispatchQueue.main.async {
completed()
}
print("Downloading Updated JSON (Version \(posts.meta.version))")
print(posts.bonuses.map {$0.bonusCode})
print(posts.bonuses.map {$0.state})
self?.bonuses = posts.bonuses
self?.defaults.set("downloadJSON", forKey: "jsonVersion") //Set version of JSON for comparison later
DispatchQueue.main.async {
//reload table in the main queue
self?.tableView.reloadData()
}
} catch {
print("JSON Download Failed")
}
}
}.resume()
}
func checkJSON() {
//MARK: Check for updated JSON file
let defaults = UserDefaults.standard
let hostedJSONFile = "http://tourofhonor.com/BonusData.json"
let jsonURL = URL(string: hostedJSONFile)
var hostedJSONVersion = ""
let jsonData = try! Data(contentsOf: jsonURL!)
let jsonFile = try! JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) as! [String : Any]
let metaData = jsonFile["meta"] as! [String : Any]
hostedJSONVersion = metaData["version"] as! String
let localJSONVersion = defaults.string(forKey: "jsonVersion")
if localJSONVersion != hostedJSONVersion {
print("L:\(localJSONVersion!) / H:\(hostedJSONVersion)")
print("Version Mismatch: Retrieving lastest JSON from server.")
updateJSONFile()
} else {
//Retrieve the existing JSON from documents directory
print("L:\(localJSONVersion!) / H:\(hostedJSONVersion)")
print("Version Match: Using local file.")
let fileURL = defaults.url(forKey: "pathForJSON")
do {
let localJSONFileData = try Data(contentsOf: fileURL!, options: [])
let myJson = try JSONSerialization.jsonObject(with: localJSONFileData, options: .mutableContainers) as! [String : Any]
//Use my downloaded JSON file to do stuff
print(myJson)
DispatchQueue.main.async {
//reload table in the main queue
self.tableView.reloadData()
}
} catch {
print(error)
}
}
}
func updateJSONFile() {
print("updateJSONFile Method Called")
let hostedJSONFile = "http://tourofhonor.com/BonusData.json"
let jsonURL = URL(string: hostedJSONFile)
let itemName = "BonusData.json"
let defaults = UserDefaults.standard
do {
let directory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false)
let fileURL = directory.appendingPathComponent(itemName)
let jsonData = try Data(contentsOf: jsonURL!)
let jsonFile = try JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) as? [String : Any]
let metaData = jsonFile!["meta"] as! [String : Any]
let jsonVersion = metaData["version"]
print("JSON VERSION ", jsonVersion!)
try jsonData.write(to: fileURL, options: .atomic)
defaults.set(fileURL, forKey: "pathForJSON") //Save the location of your JSON file to UserDefaults
defaults.set(jsonVersion, forKey: "jsonVersion") //Save the version of your JSON file to UserDefaults
DispatchQueue.main.async {
//reload table in the main queue
self.tableView.reloadData()
}
} catch {
print(error)
}
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? BonusDetailViewController {
destination.bonus = bonuses[(tableView.indexPathForSelectedRow?.row)!]
}
}
}
And here is the JsonFile.swift:
import Foundation
struct JsonFile: Codable {
struct Meta: Codable {
let fileName: String
let version: String
}
struct JsonBonuses: Codable {
let bonusCode: String
let category: String
let name: String
let value: Int
let city: String
let state: String
let flavor: String
let imageName: String
}
let meta: Meta
let bonuses: [JsonBonuses]
}
Is this related to not using JSONDecoder() in my updated version or am I going down the wrong path there? Also, how do I get this new data to work with the UITableView?
First of all, you are parsing JSON values incorrectly. You need to first understand your JSON format. You go to your JSON file link, and analyze it. If it starts with a "{", then it is a Dictionary, if it starts with a "[", then it is an Array. In your case, it is a Dictionary, then there come the keys which are Strings ("meta", "bonuses"). So, we know our keys are Strings. Next, we look at our values. For "meta" we have a Dictionary of String : String; for "bonuses" we have an Array of Dictionaries.
So, our JSON format is [String : Any], or it can be written Dictionary<String, Any>.
Next step, is accessing those values in the Dictionary.
func updateJSONFile() {
print("updateJSONFile Method Called")
let hostedJSONFile = "http://tourofhonor.com/BonusData.json"
let jsonURL = URL(string: hostedJSONFile)
let itemName = "BonusData.json"
let defaults = UserDefaults.standard
do {
let directory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false)
let fileURL = directory.appendingPathComponent(itemName)
let jsonData = try Data(contentsOf: jsonURL!)
let jsonFile = try JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) as? [String : Any]
let metaData = jsonFile!["meta"] as! [String : Any]
let jsonVersion = metaData["version"]
print("JSON VERSION ", jsonVersion!)
try jsonData.write(to: fileURL, options: .atomic)
defaults.set(fileURL, forKey: "pathForJSON") //Save the location of your JSON file to UserDefaults
defaults.set(jsonVersion, forKey: "jsonVersion") //Save the version of your JSON file to UserDefaults
DispatchQueue.main.async {
//reload table in the main queue
self.tableView.reloadData()
}
} catch {
print(error)
}
}
Then, when you access your locally saved file, again, you have to parse the JSON to check the versions:
func checkJSON() {
//MARK: Check for updated JSON file
let defaults = UserDefaults.standard
let hostedJSONFile = "http://tourofhonor.com/BonusData.json"
let jsonURL = URL(string: hostedJSONFile)
var hostedJSONVersion = ""
let jsonData = try! Data(contentsOf: jsonURL!)
let jsonFile = try! JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) as! [String : Any]
let metaData = jsonFile["meta"] as! [String : Any]
hostedJSONVersion = metaData["version"] as! String
let localJSONVersion = defaults.string(forKey: "jsonVersion")
if localJSONVersion != hostedJSONVersion {
print("\(localJSONVersion) : \(hostedJSONVersion)")
updateJSONFile()
} else {
//Retrieve the existing JSON from documents directory
print("\(localJSONVersion) : \(hostedJSONVersion)")
print("Local JSON is still the latest version")
let fileUrl = defaults.url(forKey: "pathForJSON")
do {
let localJSONFileData = try Data(contentsOf: fileUrl!, options: [])
let myJson = try JSONSerialization.jsonObject(with: localJSONFileData, options: .mutableContainers) as! [String : Any]
//Use my downloaded JSON file to do stuff
DispatchQueue.main.async {
//reload table in the main queue
self.tableView.reloadData()
}
} catch {
print(error)
}
}
}
Don't forget to allow arbitrary loads in your Info.plist file, because your JSON file is hosted on a website without https.

Loading Serialised JSON Into Table?

Im having a really hard time wrapping my head around this process, I have made an API call and received back JSON, I then use a model to serialise the JSON so it can be used easily in my View Controller Table, but im having issues with how to call the API in the View Controller to have the result fit into my serialisation model. I hope I explained it correctly?
Here is the code of my API Request:
open class ApiService: NSObject {
open func getData(completionHandler: #escaping (NSDictionary?, NSError?) -> Void) -> Self {
let requestUrl = "https://wger.de/api/v2/exercise/?format=json"
Alamofire.request(requestUrl, method: .get, encoding: URLEncoding.default)
.responseJSON { response in
switch response.result {
case .success( let data):
completionHandler(data as? NSDictionary, nil)
case .failure(let error):
print("Request failed with error: \(error)")
completionHandler(nil, error as NSError?)
}
}
return self
}
}
and here is the code of my serialisation
final public class Exercise: ResponseObjectSerializable {
var id: Int!
var description: String!
var name: String!
var muscles: String!
var equipment: String!
public init?(response: HTTPURLResponse, representation: Any) {
guard
let representation = representation as? [String: Any],
let id = representation["id"] as? Int,
let description = representation["description"] as? String,
let name = representation["name"] as? String,
let muscles = representation["muscles"] as? String,
let equipment = representation["equipment"] as? String
else { return nil }
self.id = id
self.description = description
self.name = name
self.muscles = muscles
self.equipment = equipment
}
}
But I cant work out how to fit this into my view controller function call which is currently this
let apiService = ApiService()
let searchController = UISearchController(searchResultsController: nil)
var arrRes: [String] = []
var filtered: [String] = []
var searchActive: Bool = false
var id: Int?
var description: String?
var name: String?
var muscles: String?
var equipment: String?
override func viewDidLoad() {
super.viewDidLoad()
exercisesTableView.delegate = self
exercisesTableView.dataSource = self
exerciseSearchBar.delegate = self
getApiData()
}
func getApiData() {
let _ = apiService.getData() {
(data, error) in
if let data = data {
if let arr = data["results"] as? [String] {
self.arrRes = arr
self.exercisesTableView.reloadData()
}
} else if let error = error {
print(error)
}
}
}
First of all the HTTP response does not affect the custom class at all so I left it out.
Second of all the values for keys muscles and equipment are arrays rather than strings.
Third of all since the JSON data seems to be immutable declare the properties in the class as constant (let)
With a few slightly changes this is the custom class
final public class Exercise {
let id : Int
let description: String
let name: String
let muscles : [Int]
let equipment : [Int]
public init?(dictionary: [String: Any]) {
guard
let id = dictionary["id"] as? Int,
let description = dictionary["description"] as? String,
let name = dictionary["name"] as? String,
let muscles = dictionary["muscles"] as? [Int],
let equipment = dictionary["equipment"] as? [Int]
else { return nil }
self.id = id
self.description = description
self.name = name
self.muscles = muscles
self.equipment = equipment
}
}
Then you have to declare the data source array
var exercises = [Exercise]()
And in the method getApiData() populate the array
...
if let results = data["results"] as? [[String:Any]] {
for result in results {
if let exercise = Exercise(dictionary: result) {
self.exercises.append(exercise)
}
}
self.exercisesTableView.reloadData() // might be dispatched on the main thread.
}
Note: Any is used in Swift 3, in Swift 2 replace all occurrences of Any with AnyObject