Reputation: 340
I'm still stuck, and I've been banging my head against a wall for weeks on this. So here I am asking one more time, this time in a more complete manner. I just want to make this work. I've been trying to use Swift 4 as much as possible (since I'm learning it seems easier to stick to one set of rules/syntax, but at this point I don't care what language is used as long as it works so I can move on to the rest of the things I need to do to the app.
Goal: Look at local version of JSON, and compare it to hosted version. If hosted is newer, replace the local version with the newer one. Then parse the local JSON file to create the UITableView, and divide it into sections by state.
Issue: It sorta worked with an old method that was parsing it live from the website, but the sections where showing duplicates and the wrong counts. It now seems to be comparing local to hosted correctly, but the UITableView is not being populated at all now. I suspect all my issues are in the tableView sections, but I've tried it 10 trillion different ways and none of them work. I assume I'm not properly pointing it to the local JSON file.
Code: Here is my entire ViewController:
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, which provides the struct for the JSON parsing:
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]
}
I need someone to explain it like I'm 5. I feel like I understand what my functions are doing, but I can't for the life of me figure out why it doesn't work, nor why when it did work (using the old method), that the sections where completely out of whack. I'm sorry if you are seeing my ask questions, I'm just trying to learn how to do this so I can be self-sufficient, but this one piece has just not been making sense to me.
Upvotes: 0
Views: 703
Reputation: 9825
Split your goal up into separate tasks and write a function for each.
You need to be able to:
Your current downloadJSON
function is close to what you want for the first one but I modified it slightly so it doesn't deal with the other parts of your controller directly and instead of just sends the bonuses back in the completion handler:
func downloadJSON(completed: @escaping ([JsonFile.JsonBonuses]?) -> ()) {
let url = URL(string: "http://tourofhonor.com/BonusData.json")!
URLSession.shared.dataTask(with: url) { (data, response, error) in
if error == nil, let data = data {
do {
let posts = try JSONDecoder().decode(JsonFile.self, from: data)
completed(posts.bonuses)
} catch {
print("JSON Download Failed")
}
} else {
completed(nil)
}
}.resume()
}
Saving your json to a file is simple because your objects implement Codable
:
func saveBonuses(_ bonuses: [JsonFile.JsonBonuses], to url: URL) {
try? FileManager.default.removeItem(at: url)
do {
let data = try JSONEncoder().encode(bonuses)
try data.write(to: url)
} catch {
print("Error saving bonuses to file:", error)
}
}
Similar with loading from a file:
func loadBonusesFromFile(_ url: URL) -> [JsonFile.JsonBonuses]? {
do {
let data = try Data(contentsOf: url)
let bonuses = try JSONDecoder().decode([JsonFile.JsonBonuses].self, from: data)
return bonuses
} catch {
print("Error loading bonuses from file:", error)
return nil
}
}
These parts are all independent so now you need another function with logic that ties them together. We want to attempt to grab the json from the server and save it to a file, or if that fails load any json that was saved to a file previously and use that:
func loadBonuses(completion: @escaping ([JsonFile.JsonBonuses]?) -> Void) {
let localBonusesURL = try! FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("Bonuses.json")
downloadJSON { bonuses in
if let bonuses = bonuses {
completion(bonuses)
saveBonuses(bonuses, to: localBonusesURL)
} else {
completion(loadBonusesFromFile(localBonusesURL))
}
}
}
Now you can use this new loadBonuses
function when you load your view controller:
override func viewDidLoad() {
super.viewDidLoad()
loadBonuses { [weak self] bonuses in
self?.bonuses = bonuses ?? []
self?.tableView.reloadData()
}
}
Upvotes: 1
Reputation: 6969
Before delving into how iOS UITableView works, get this straight:
Now:
So first, think how you are displaying bonuses. Are they flat list (array), or grouped into some bigger chunks?
If it has no classification:
numberOfSections
method must return 1. numberOfRowsInSection
must return bonuses.count
. And most importantly, your cellForRowAt
should look like this (notice that bonuses array is indexed by row index, not section index):
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
cell.textLabel?.text = bonuses[indexPath.row].name.capitalized
return cell
}
If it has classification, then you must think of bonuses
as array of arrays.
numberOfSections
will return bonuses.count
- the number of arrays.numberOfRowsInSection
will fetch an array (let's say x[] - note that x itself is an array) element from within bonuses array, and return x.countYour cellForRowAt
will again fetch an array element from bonuses (let's say x[]
). Then, it will fetch the row item from with in x, like: x[indexPath.row]
, and your final code will look like (I have omitted unwrapping etc as compiler will tell you anyway):
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
let x = bonuses[indexPath.section]
let bonusItem = x[indexPath.row]
cell.textLabel?.text = bonusItem.name.capitalized
return cell
}
Upvotes: 1