I have been working on a launch database for SpaceX and I have successfully parsed my data but the function to create and add the data to the cell is not working. I have added the delegates and data sources but I still cannot find out why it won't run.
import UIKit
struct launchData : Decodable
{
let flight_number : Int
let launch_date_utc : String
struct rocketInfo : Decodable
{
let rocket_name : String
}
let rocket : rocketInfo
}
class LaunchViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var launchTableView: UITableView!
var arrayOfLaunchData:[launchData] = []
override func viewDidLoad()
{
super.viewDidLoad()
self.launchTableView.delegate = self
self.launchTableView.dataSource = self
getJsonData()
self.launchTableView.reloadData()
}
func getJsonData()
{
let jsonUrlString = "https://api.spacexdata.com/v2/launches"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let launchDataDecoded = try JSONDecoder().decode([launchData].self, from: data)
print(launchDataDecoded)
} catch let jsonErr {
print("Error Serialization json:", jsonErr )
}
}.resume()
print("getJsonData ran")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("")
print(arrayOfLaunchData.count)
print("")
print("TableView number of rows ran")
return arrayOfLaunchData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CellID")
let launch = self.arrayOfLaunchData[indexPath.row]
let flightNumber = launch.flight_number
let rocketName = launch.rocket.rocket_name
cell?.textLabel?.text = "Mission " + String(flightNumber)
let launchDate = launch.launch_date_utc
cell!.detailTextLabel!.text = "Launch Date: " + launchDate + "Rocket Used: " + rocketName
self.launchTableView.reloadData()
print("TableView cellForRowAt ran")
return cell!
}
}
First of all never call reloadData() in cellForRowAt! Delete the line
Two major issues:
reloadData() is called too soon.
The data source array is not populated after receiving the data.
The solution is to delete the line
self.launchTableView.reloadData()
(also) in viewDidLoad() and change getJsonData() to
func getJsonData()
{
let jsonUrlString = "https://api.spacexdata.com/v2/launches"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
self.arrayOfLaunchData = try JSONDecoder().decode([launchData].self, from: data)
print(launchDataDecoded)
DispatchQueue.main.async {
self.launchTableView.reloadData()
}
} catch {
print("Error Serialization json:", error )
}
}.resume()
print("getJsonData ran")
}
because dataTask works asynchronously.
Note:
Please conform to the naming convention that struct and class names start with a capital letter (LaunchData, RocketInfo) and all names are supposed to be camelCased rather than snake_cased.
Remove self.launchTableView.reloadData() from viewDidLoad()
and put on getting successfully data
do {
let launchDataDecoded = try JSONDecoder().decode([launchData].self, from: data)
print(launchDataDecoded)
self.launchTableView.reloadData()
} catch let jsonErr {
print("Error Serialization json:", jsonErr )
}
}.resume()
getJsonData() is follow asynchronous. hope this help!
Related
I'm working with CocktailDB.
By creating a request I get a JSON file, parse it with Decodable protocol. From JSON I get all drinks' categories and display them as the sections of my tableview.
In each tableview section I want to display drinks from specific category (section's header). One drink per section cell from the category (drink's strDrink (name) and strDrinkThumb (image)).
I have a method that creates a request to get drinks from specific category - getDrinksFrom(category: String).
Please advice how can I call this method for specific section to get and display drinks from specific category in this section?
My code:
class ViewController: UIViewController {
var drinks = [Drink]()
var categories = [Category]()
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
getCategories()
getDrinksFrom(category: "Cocoa")
}
func getCategories() {
let url = URL(string: "https://www.thecocktaildb.com/api/json/v1/1/list.php?c=list")
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error == nil {
do {
self.categories = try JSONDecoder().decode(Categories.self, from: data!).drinks
DispatchQueue.main.async {
self.tableView.reloadData()
}
print(self.categories)
} catch {
print(error)
}
}
}.resume()
}
func getDrinksFrom(category: String) {
let url = URL(string: "https://www.thecocktaildb.com/api/json/v1/1/filter.php?c=\(category)")
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error == nil {
do {
self.drinks = try JSONDecoder().decode(Drinks.self, from: data!).drinks
DispatchQueue.main.async {
self.tableView.reloadData()
}
print(self.drinks)
} catch {
print(error)
}
}
}.resume()
}
}
extension ViewController: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
return categories.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return categories[section].strCategory
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "drinkCell") as! DrinkCell
cell.drinkName.text = drinks[indexPath.row].strDrink
let url = drinks[indexPath.row].strDrinkThumb
cell.drinkImage.downloaded(from: url)
return cell
}
}
// to download an image from web
extension UIImageView {
func downloaded(from url: URL, contentMode mode: UIView.ContentMode = .scaleAspectFit) {
contentMode = mode
URLSession.shared.dataTask(with: url) { data, response, error in
guard
let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
let data = data, error == nil,
let image = UIImage(data: data)
else { return }
DispatchQueue.main.async() { [weak self] in
self?.image = image
}
}.resume()
}
func downloaded(from link: String, contentMode mode: UIView.ContentMode = .scaleAspectFit) {
guard let url = URL(string: link) else { return }
downloaded(from: url, contentMode: mode)
}
}
Category Model:
struct Categories:Decodable {
var drinks: [Category]
}
struct Category:Decodable {
var strCategory: String
}
Drink Model:
struct Drinks:Decodable {
var drinks: [Drink]
}
struct Drink:Decodable {
var strDrink: String
var strDrinkThumb: String
}
What I have for know:
JSON structure:
My suggestion is to create a custom struct Category with name and drinks for the sections. It does not conform to Decodable, this is intended
struct Category {
let name : String
var drinks : [Drink]
}
and an appropriate data source array
var categories = [Category]()
then load and parse the categories with traditional JSONSerialization and populate the array by mapping the names. Further add a completion handler
func getCategories(completion: #escaping () -> Void) {
let url = URL(string: "https://www.thecocktaildb.com/api/json/v1/1/list.php?c=list")
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if let error = error { print(error); return }
do {
let result = try JSONSerialization.jsonObject(with: data!) as! [String:Any]
let categoryNames = result["drinks"] as! [[String:String]]
self.categories = categoryNames.map{ Category(name: $0["strCategory"]!, drinks:[])}
completion()
} catch {
print(error)
}
}.resume()
}
To avoid naming confusion (too many drinks) name the root struct Response
struct Response : Decodable {
let drinks: [Drink]
}
Load the data related to a category and assign the drinks array to the corresponding array in categories
func getDrinksFrom(category: String) {
let url = URL(string: "https://www.thecocktaildb.com/api/json/v1/1/filter.php?c=\(category)")
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if let error = error { print(error); return }
do {
let drinks = try JSONDecoder().decode(Response.self, from: data!).drinks
guard let index = categories.firstIndex(where: {$0.name == category}) else { return }
self.categories[index].drinks = drinks
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch {
print(error)
}
}.resume()
}
and replace viewDidLoad with
override func viewDidLoad() {
super.viewDidLoad()
getCategories { [weak self] in
self?.getDrinksFrom(category: "Cocoa")
}
}
Finally change the table view data source methods to match the section structure
extension ViewController: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
return categories.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return categories[section].name
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return categories[section].drinks.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "drinkCell") as! DrinkCell
let category = categories[indexPath.section]
let drink = category.drinks[indexPath.row]
cell.drinkName.text = drink.strDrink
let url = drink.strDrinkThumb
cell.drinkImage.downloaded(from: url)
return cell
}
}
You can also put both functions together and load all drinks for all categories
func loadAllCategories() {
let url = URL(string: "https://www.thecocktaildb.com/api/json/v1/1/list.php?c=list")
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if let error = error { print(error); return }
do {
let result = try JSONSerialization.jsonObject(with: data!) as! [String:Any]
let categoryNames = (result["drinks"] as! [[String:String]]).map{$0["strCategory"]!}
let group = DispatchGroup()
for category in categoryNames {
let categoryURLString = "https://www.thecocktaildb.com/api/json/v1/1/filter.php?c=\(category)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let categoryURL = URL(string: categoryURLString)!
group.enter()
let categoryTask = URLSession.shared.dataTask(with: categoryURL) { (categoryData, _, categoryError) in
defer { group.leave() }
if let categoryError = categoryError { print(categoryError); return }
do {
let drinks = try JSONDecoder().decode(Response.self, from: categoryData!).drinks
self.categories.append(Category(name: category, drinks: drinks))
} catch {
print(error)
}
}
categoryTask.resume()
}
group.notify(queue: .main) {
self.tableView.reloadData()
}
} catch {
print(error)
}
}.resume()
}
This is just a pseudocode, which will give you an idea how you can proceed further. The code has not been tested.
Create an array of sections to be loaded.
var sections: [Sections] = []
In you tableview delegates you can create a struct for the sections that you need to load, which will help you to identify the section in cell for row index path where you can call API based on categories.
extension ViewController: UITableViewDataSource, UITableViewDelegate {
struct Sections {
static var count = 0
// In stantiate table view headers index order
enum SectionType {
case SoftDrink
case OrdinaryDrink
case MilkShake
}
var type: SectionType?
var section: Int?
var rows: Int?
}
func setUpTableView() {
// Set Up Tableview Data
if check if Drink is type of SoftDrink /*If you sections are loaded dynamic u can add condition*/ {
sections.append(Sections(type: .SoftDrink, section: Sections.count, rows: 1))
Sections.count += 1
}
Sections.count = 0
}
func numberOfSections(in _: UITableView) -> Int {
sections.count
}
func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
sections[section].rows ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var tableCell: UITableViewCell = UITableViewCell()
guard let type = sections[indexPath.section].type else {
tableCell.selectionStyle = .none
return tableCell
}
switch type {
case .SoftDrink: break
// Instantiate cell and API calls.
case .OrdinaryDrink: break
// Instantiate cell and API calls.
case .MilkShake: break
// Instantiate cell and API calls.
}
tableCell.selectionStyle = .none
return tableCell
}
}
setUpTableView() can be called in viewDidLoad Method.
I have this here structured:
struct Excercise: Codable {
let excerciseID: String
let excerciseName: String
let description: String
let intensity: Int
}
Then, that's the var:
var excercistList = [Excercise]()
And the cellforrowat:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ExcerciseCell", for: indexPath)
cell.textLabel?.text = self.excercistList[indexPath.row].excerciseName
return cell
}
That's the URLSession:
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else{return}
do {
let excerciseList = try JSONDecoder().decode(Excercise.self, from: data)
print(excerciseList.excerciseName)
} catch let jsonErr {
print("Error", jsonErr)
}
}.resume()
I get the right results on the print, but nothing on the table.
What am I doing wrong?
first of all you have to populate your dataSource which is var excercistList = [Excercise](). After that you have to reload tableView.
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else{return}
do {
let excerciseListFromDecoder = try JSONDecoder().decode(Excercise.self, from: data)
self.excercistList = excerciseListFromDecoder
self.tableView.reloadData()
} catch let jsonErr {
print("Error", jsonErr)
}
}.resume()
You have to be sure that you have set correctly
tableView.delegate = self
tableView.dataSource = self
and the numberOfRowsInSection
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.excercistList.count
}
I'm trying to retrieve a 5 day forecast using the OpenWeatherMap API, I'm not sure why but each time I call my weatherCount() method it returns nil.
In the view model I use a print statement to verify the number of rows should be 40. I have tried to use guard statements and force unwrapping which just crashes the program. I tried implementing callback methods but don't think I did them correctly.
WeatherViewModel
import Foundation
class WeatherViewModel {
var weatherInfo: WeatherData?
weak var delegate: WeatherDelegate?
func getWeatherData() {
let weather = "https://api.openweathermap.org/data/2.5/forecast?q=London,GB&appid=fe3e0ecae7e573d25b37542f96f66f1a"
guard let url = URL(string: weather) else {
print("Could not reach the API endpoint") // this guard is not being hit
return }
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in //data task object, completion handler(trailing closure)
DispatchQueue.main.async {
guard error == nil else { // Checking for errors in the request
print("Error retrieved was: \(error)")
return
}
guard let weatherResponse = data else { //checks we got the data from request
print("Could not retrieve data instead got \(data)")
return }
}
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let responseData = try decoder.decode(WeatherData.self, from: data!)
DispatchQueue.main.async {
// print("Delegate method shows \(self.delegate?.didRecieve(forecast: responseData))")
self.weatherInfo = responseData
print(self.weatherInfo)
print("Number of rows in section will be : \(self.weatherInfo?.list.count ?? 1)")
}
}
catch let e as Error {
print("Error creating current weather from JSON because: \(e.localizedDescription)")
print("Error in parsing the JSON")
NSLog("Error hit when calling weather service \(e)")
}
}
task.resume()
}
func weatherCount() -> Int {
let numberOfRows = self.weatherInfo?.list.count
print("Number of rows in weatherCount : \(numberOfRows)")
return numberOfRows ?? 1
}
}
WeatherTableViewController
import UIKit
import Foundation
class WeatherTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet var tableView: UITableView!
lazy var viewModel: WeatherViewModel = {
return WeatherViewModel()
}()
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
DispatchQueue.main.async {
self.viewModel.getWeatherData()
self.tableView.reloadData()
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 5
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//print("Number of rows in section is: \(viewModel.weatherInfo?.list.count)")
//print("Rows: \(viewModel.weatherCount())")
return viewModel.weatherCount() ?? 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let weatherCell = tableView.dequeueReusableCell(withIdentifier: "WeatherCell", for: indexPath)
weatherCell.textLabel?.text = " The current temperature is : \(viewModel.weatherInfo?.list[indexPath.row].main?.temp ?? 0)"
print(viewModel.weatherInfo?.list[indexPath.row].main?.temp)
return weatherCell
}
}
numberOfRowsInSection should return 40 however returns nil
Weather
import Foundation
struct WeatherData: Decodable {
let cod: String
let message: Double
let cnt: Int
let list: [Info]
}
struct Info: Decodable {
let dt: Date
let main: WeatherInfo?
}
struct WeatherInfo: Decodable {
let temp: Double
let temp_min: Double
let temp_max: Double
let pressure: Double
let sea_level: Double
let grnd_level: Double
let humidity: Int
let temp_kf: Double
}
private enum CodingKeys: String, CodingKey {
case minTemp = "temp_min"
case maxTemp = "temp_max"
case seaLevel = "sea_level"
case temp
case pressure
case groundLevel = "grnd_level"
case humidity
case temp_kf
}
Use a completion handler to get notify from the weather data parsing and then reload tableView as below,
func getWeatherData(_ completion: #escaping () -> Void) {
let weather = "https://api.openweathermap.org/data/2.5/forecast?q=London,GB&appid=fe3e0ecae7e573d25b37542f96f66f1a"
guard let url = URL(string: weather) else {
print("Could not reach the API endpoint") // this guard is not being hit
return }
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in //data task object, completion handler(trailing closure)
guard error == nil else { // Checking for errors in the request
print("Error retrieved was: \(error)")
return
}
guard let weatherResponse = data else { //checks we got the data from request
print("Could not retrieve data instead got \(data)")
return
}
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let responseData = try decoder.decode(WeatherData.self, from: data!)
DispatchQueue.main.async {
// print("Delegate method shows \(self.delegate?.didRecieve(forecast: responseData))")
self.weatherInfo = responseData
completion()
}
}
catch let e as Error {
print("Error creating current weather from JSON because: \(e.localizedDescription)")
print("Error in parsing the JSON")
NSLog("Error hit when calling weather service \(e)")
}
}
task.resume()
}
Update viewDidLoad as,
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
self.viewModel.getWeatherData() {
self.tableView.reloadData()
}
}
I am getting
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
at DispatchQueue.main.async{ self.tableView.reloadData() }
My code
import UIKit
class BrowseRequestsViewController: UIViewController, UITableViewDelegate,UITableViewDataSource {
final let url = URL(string: "")
private var browseRequestDataModel = [Datum]()
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
downloadJson()
}
func downloadJson() {
guard let downloadURL = url else { return }
URLSession.shared.dataTask(with: downloadURL) { data, urlResponse, error in
guard let data = data, error == nil, urlResponse != nil else {
print("something is wrong")
return
}
print("downloaded")
do {
let decoder = JSONDecoder()
let downloadedBrowseRequestData = try decoder.decode(BrowseRequestsDataModel.self, from: data)
self.browseRequestDataModel = downloadedBrowseRequestData.data
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch {
print("something wrong after downloaded")
}
}.resume()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return browseRequestDataModel.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "BrowseRequestsTableViewCell" ) as? BrowseRequestsTableViewCell else { return UITableViewCell() }
cell.shipperName.text = browseRequestDataModel[indexPath.row].user.name
cell.pickupLocation.text = browseRequestDataModel[indexPath.row].pickupLocation.pickupLocationCity + " , " + browseRequestDataModel[indexPath.row].pickupLocation.pickupLocationCountry
cell.dropoffLocation.text = browseRequestDataModel[indexPath.row].dropoffLocation.dropoffLocationCity + " , " + browseRequestDataModel[indexPath.row].dropoffLocation.dropoffLocationCountry
cell.item.text = browseRequestDataModel[indexPath.row].item.name
cell.pickupDate.text = browseRequestDataModel[indexPath.row].pickupDate
cell.dropoffDate.text = browseRequestDataModel[indexPath.row].dropoffDate
return cell
}
}
I finally started updating the BonusData.json file that my app uses. Now I am getting an error when it tries to load the data. The complete code is below but I am getting "JSON Download Failed", which is contained in the downloadJSON function.
If I'm reading my code right, that would mean that I'm encountering an error in the
let posts = try JSONDecoder().decode(JsonFile.self, from: data)
completed(posts.bonuses)
section, but I'm not sure how to troubleshoot that any further. What is supposed to be happening is that the app looks at the server, downloads the JSON and then saves it locally to be used to populate the UITableView. If there is no data connection, then it should not care, and just use the local saved version. Because the app is loading up blank, I'm assuming it is also not working as it is intended.
Here is the complete code:
import UIKit
import os.log
import Foundation
class BonusListViewController: UITableViewController {
var bonuses = [JsonFile.JsonBonuses]()
var filteredBonuses = [JsonFile.JsonBonuses]()
var detailViewController: BonusDetailViewController? = nil
let defaults = UserDefaults.standard
let searchController = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
super.viewDidLoad()
// MARK: Search Support
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Enter two letter state to filter"
navigationItem.searchController = searchController
definesPresentationContext = true
// MARK: Settings Data Struct
struct Constants {
struct RiderData {
let riderNumToH = "riderNumToH"
let pillionNumToH = "pillionNumToH"
}
struct RallyData {
let emailDestinationToH = "emailDestinationToH"
}
}
//MARK: Load the bonuses
print("About to call loadBonuses")
loadBonuses { [weak self] bonuses in
self?.bonuses = bonuses ?? []
DispatchQueue.main.async {
self?.tableView.reloadData()
}
print("loadBonuses called")
}
}
// MARK: - Table View Configuration
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isFiltering() {
print("Showing \(filteredBonuses.count) Filtered Results")
return filteredBonuses.count
}
print("Found \(bonuses.count) rows in section.")
return bonuses.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "BonusListViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? BonusListViewCell else {
fatalError("The dequeued cell is not an instance of BonusListViewCell.")
}
// let bonus = bonuses[indexPath.row]
let bonus: JsonFile.JsonBonuses
if isFiltering() {
bonus = filteredBonuses[indexPath.row]
} else {
bonus = bonuses[indexPath.row]
}
let urlString = "http://tourofhonor.com/appimages/"+(bonus.imageName)
let url = URL(string: urlString)
cell.primaryImage.downloadedFrom(url: url!)
cell.nameLabel.text = bonus.name.capitalized
cell.bonusCodeLabel.text = bonus.bonusCode.localizedUppercase
cell.categoryLabel.text = bonus.category
cell.valueLabel.text = "\(bonus.value)"
cell.cityLabel.text = "\(bonus.city.capitalized),"
cell.stateLabel.text = bonus.state.localizedUppercase
return cell
}
// MARK: Functions
// MARK: - Fetch JSON from ToH webserver
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)
print("URLSession did not fail")
} catch {
print("JSON Download Failed")
}
} else {
print("downloadJSON completed")
completed(nil)
}
}.resume()
}
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)
print("saveBonuses successful")
} catch {
print("Error saving bonuses to file:", error)
}
}
func loadBonusesFromFile(_ url: URL) -> [JsonFile.JsonBonuses]? {
do {
let data = try Data(contentsOf: url)
let bonuses = try JSONDecoder().decode([JsonFile.JsonBonuses].self, from: data)
print("loadBonusesFromFile successful")
return bonuses
} catch {
print("Error loading bonuses from file:", error)
return nil
}
}
func loadBonuses(completion: #escaping ([JsonFile.JsonBonuses]?) -> Void) {
let localBonusesURL = try! FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("BonusData.json")
downloadJSON { bonuses in
if let bonuses = bonuses {
completion(bonuses)
self.saveBonuses(bonuses, to: localBonusesURL)
} else {
print("versions did not match")
completion(self.loadBonusesFromFile(localBonusesURL))
}
}
}
func searchBarIsEmpty() -> Bool {
// Returns true if the text is empty or nil
return searchController.searchBar.text?.isEmpty ?? true
}
func filterContentForSearchText(_ searchText: String, scope: String = "All") {
filteredBonuses = bonuses.filter({( bonus: JsonFile.JsonBonuses) -> Bool in
return bonus.state.localizedCaseInsensitiveContains(searchText)
})
tableView.reloadData()
}
func isFiltering() -> Bool {
return searchController.isActive && !searchBarIsEmpty()
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? BonusDetailViewController {
destination.bonus = bonuses[(tableView.indexPathForSelectedRow?.row)!]
}
}
}
extension BonusListViewController: UISearchResultsUpdating {
// MARK: - UISearchResultsUpdating Delegate
func updateSearchResults(for searchController: UISearchController) {
filterContentForSearchText(searchController.searchBar.text!)
}
}
The JSON is hosted here: http://tourofhonor.com/BonusData.json
It looks like the JSON you're trying to download is not formatted correctly. It's missing a comma between objects, and it has an extra comma at the end of the list.
There are a number of tools to validate JSON, but one accessible one is https://jsonlint.com/. If you paste the output from http://tourofhonor.com/BonusData.json there, it will highlight the formatting errors for you and give you some guidance on how to fix them.
I'm going to focus on what I think is the core of your question rather than the technical fix.
I'm not sure how to troubleshoot that any further.
do {
// ...
let posts = try JSONDecoder().decode(JsonFile.self, from: data)
// ...
} catch let error {
// Do something with this error.
}
decode throws details about the exception which you can do when you get the error.