UITableView with Sections as Date from Json Data - json

I am working on table view to render some data received after JSON parsing. I want my table view to have sections based on different dates. Each record in JSON is an event and multiple events can take place on single date.
Here is my JSON data
https://get.rosterbuster.com/wp-content/uploads/dummy-response.json
I want to render my table view like this
Table View with Sections as Date
What I have done sofar:
I have parsed the data in following Structure
struct Roster : Codable {
let flightnr: String?
let date: String?
let aircraftType: String?
let tail: String?
let departure: String?
let destination: String?
let departTime: String?
let arrivalTime: String?
let dutyID: String?
let dutyCode: String?
let captain: String?
let firstOfficer: String?
let flightAttendant: String?
enum CodingKeys: String, CodingKey {
case flightnr = "Flightnr"
case date = "Date"
case aircraftType = "Aircraft Type"
case tail = "Tail"
case departure = "Departure"
case destination = "Destination"
case departTime = "Time_Depart"
case arrivalTime = "Time_Arrive"
case dutyID = "DutyID"
case dutyCode = "DutyCode"
case captain = "Captain"
case firstOfficer = "First Officer"
case flightAttendant = "Flight Attendant"
}
}
I have also setup basic table view but don't know how to group the retrieved data into different sections as per the image I have attached above.
Any help would be appreciated.

This is the approach I'd suggest:
1) get number of sections by mapping the API JSON response in a set based on the date property. Here's something you could use (maybe you don't need to cast it in Array as well and you want to check if date is not nil)
self.sections = Array(Set(self.dataModel.map({ (roster) -> String in
roster.date!
})))
2) set your rowsPerSection data model by creating an array of Roster for each section.
//first set the array of sections.count dimension and empty array for each item
self.sections.forEach({ (string) in
self.rowsPerSection.append([])
})
//then set each array
for index in 0..<self.sections.count {
self.dataModel.forEach({ (roster) in
if roster.date == self.sections[index] {
self.rowsPerSection[index].append(roster)
}
})
}
This is my dummy code, I tested it with your URL and it works:
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var dataModel = [Roster]()
var sections = [String]()
var rowsPerSection = [[Roster]]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
APICall { (rosters) in
DispatchQueue.main.async {
self.dataModel = rosters!
self.sections = Array(Set(self.dataModel.map({ (roster) -> String in
roster.date!
})))
//first set the array of sections.count dimension and empty array for each item
self.sections.forEach({ (string) in
self.rowsPerSection.append([])
})
//then set each array
for index in 0..<self.sections.count {
self.dataModel.forEach({ (roster) in
if roster.date == self.sections[index] {
self.rowsPerSection[index].append(roster)
}
})
}
self.tableView.reloadData()
}
}
}
func APICall(onSuccess: #escaping(_ response: [Roster]?) -> Void) {
let group = DispatchGroup()
group.enter()
DispatchQueue.global(qos: .default).async {
let url = URL(string: "https://get.rosterbuster.com/wp-content/uploads/dummy-response.json")!
let requestURL = URLRequest(url: url)
let session = URLSession.shared
session.dataTask(with: requestURL) { (data, response, error) in
let decoder = JSONDecoder()
let responseJson = try! decoder.decode([Roster].self, from: data!)
onSuccess(responseJson)
group.leave()
}.resume()
group.wait()
return
}
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
for index in 0..<sections.count {
if index == section {
return rowsPerSection[index].count
}
}
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = rowsPerSection[indexPath.section] [indexPath.row].destination
return cell
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sections[section]
}
}
Here's the screenshot -> screenshot

Related

Swift Table view Fail to Load the data

I am new in swift . I am trying the display the data into tableview controller from API . Here is the api link . https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos?sol=1000&api_key=DEMO_KEY. I create model based on the json property . Here is the model code .
import Foundation
struct Welcome: Codable {
let photos: [Photo]
}
// MARK: - Photo
struct Photo: Codable {
let id, sol: Int
let camera: Camera
let imgSrc: String
let earthDate: String
let rover: Rover
enum CodingKeys: String, CodingKey {
case id, sol, camera
case imgSrc = "img_src"
case earthDate = "earth_date"
case rover
}
}
// MARK: - Camera
struct Camera: Codable {
let id: Int
let name: String
let roverID: Int
let fullName: String
enum CodingKeys: String, CodingKey {
case id, name
case roverID = "rover_id"
case fullName = "full_name"
}
}
// MARK: - Rover
struct Rover: Codable {
let id: Int
let name, landingDate, launchDate, status: String
enum CodingKeys: String, CodingKey {
case id, name
case landingDate = "landing_date"
case launchDate = "launch_date"
case status
}
}
From this model I want to user Rover struct fields like id and status and display those property into table view cell .
Here is the network Manager code .
class NetworkManager{
func fetchData(completion: #escaping ([Rover]) -> Void) {
if let url = URL(string: "https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos?sol=1000&api_key=DEMO_KEY") {
URLSession.shared.dataTask(with: url) { data, urlResponse, error in
if let data = data {
do {
let result = try JSONDecoder().decode([Rover].self, from: data)
completion(result)
} catch let error {
print(error.localizedDescription)
}
}
}
.resume()
}
}
}
Here code for view controller .
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
private var posts = [Rover]()
private let networkManager = NetworkManager()
private var rowSelected = 0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
setUpUI ()
fetchData()
}
private func setUpUI () {
tableView.dataSource = self
tableView.delegate = self
}
private func fetchData() {
networkManager.fetchData { [weak self] array in
self?.posts = array
DispatchQueue.main.async {
self?.tableView.reloadData()
}
}
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
rowSelected = indexPath.row
performSegue(withIdentifier: "cell", sender: nil)
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return posts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let row = indexPath.row
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let post = posts[row]
cell.textLabel?.text = String(post.id)
cell.detailTextLabel?.text = post.status
return cell
}
}
Here is the console result when i run the code , it is saying The data couldn’t be read because it isn’t in the correct format.

How to decode custom type inside dictionary value with JSON?

my JSON:
https://www.cbr-xml-daily.ru/daily_json.js
my code:
struct CoinData: Decodable {
let Valute: [String: CoinInfo]
}
struct CoinInfo: Decodable {
let Name: String
let Value: Double
}
if let safeData = data {
if let coinData = self.parseJSON(safeData) {
print(coinData)
}
}
func parseJSON(_ data: Data) -> [String: CoinInfo]? {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(CoinData.self, from: data)
return decodedData.Valute
} catch {
delegate?.didFailWithError(error: error)
return nil
}
}
In debug console following gets printed:
["PLN": CurrencyConverter.CoinInfo(Name: "X", Value: 19.6678), ...]
This way I can't reach Name and Value properties of a coin. What's wrong?
I am going to do for-loop to check if a key contains certain symbols. If it does - I will need to be able to access to both Name and Value
You don't actually need a for loop. Since coinData is a dictionary, you can use its subscript, together with optional binding to do this. For example, to check if the key "PLN" exists, and access its name and value:
if let coinInfo = coinData["PLN"] {
print(coinInfo.Name)
print(coinInfo.Value)
} else {
// "PLN" does not exist
}
StoyBoard
Code
import UIKit
import Alamofire
// MARK: - CoinData
struct CoinData: Codable {
let date, previousDate: String
let previousURL: String
let timestamp: String
let valute: [String: Valute]
enum CodingKeys: String, CodingKey {
case date = "Date"
case previousDate = "PreviousDate"
case previousURL = "PreviousURL"
case timestamp = "Timestamp"
case valute = "Valute"
}
}
// MARK: - Valute
struct Valute: Codable {
let id, numCode, charCode: String
let nominal: Int
let name: String
let value, previous: Double
enum CodingKeys: String, CodingKey {
case id = "ID"
case numCode = "NumCode"
case charCode = "CharCode"
case nominal = "Nominal"
case name = "Name"
case value = "Value"
case previous = "Previous"
}
}
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate{
var getCoinData = [CoinData]()
var coinNameArr = [String]()
var coinDataArr = [Valute]()
#IBOutlet weak var tblDataList: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
getData()
}
func getData()
{
let url = "https://www.cbr-xml-daily.ru/daily_json.js"
AF.request(url, method: .get, encoding: URLEncoding.default).responseJSON { response in
let json = response.data
do{
let decoder = JSONDecoder()
self.getCoinData = [try decoder.decode(CoinData.self, from: json!)]
let response = self.getCoinData[0]
if response.valute.count != 0 {
self.coinNameArr.removeAll()
self.coinDataArr.removeAll()
for (coinName, coinData) in response.valute {
self.coinNameArr.append(coinName)
self.coinDataArr.append(coinData)
}
self.tblDataList.reloadData()
} else {
}
}catch let err{
print(err)
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return coinDataArr.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:coinTblCell = tableView.dequeueReusableCell(withIdentifier: "CellID", for: indexPath as IndexPath) as! coinTblCell
cell.accessoryType = .disclosureIndicator
cell.tintColor = .black
let rowData = coinDataArr[indexPath.row]
cell.lblName.text = rowData.name
cell.lblValue.text = String(rowData.value)
return cell
}
}
class coinTblCell: UITableViewCell {
#IBOutlet weak var lblName: UILabel!
#IBOutlet weak var lblValue: UILabel!
}

My object array is nil while my data are correct

I try to display my data in a tableView using no framework to parse my data, but when I add my data to my table and debug it, it is nil at the output while my data I retrieve are well parses, have I forgotten something to do?
I use a structure for my parameters as this :
enum Types {
case School
case Hospital
case Station_Essence
case Restaurant
}
struct Adresse {
public var title: String
public var details: String?
public var type: Types
public var coordinate: [String: Any]
}
and in my ViewController, i proced as this :
class ListMapViewController: UIViewController {
#IBOutlet var TitleTableView: UITableView!
#IBOutlet var MapView: MKMapView!
var adresse: [Adresse]?
override func viewDidLoad() {
super.viewDidLoad()
self.TitleTableView.register(UINib(nibName: "ListMapTableViewCell", bundle: nil), forCellReuseIdentifier: "Adresse")
self.TitleTableView.delegate = self
self.TitleTableView.dataSource = self
guard let POI = URL(string: "https://moc4a-poi.herokuapp.com/") else {
return
}
let task = URLSession.shared.dataTask(with: POI) { (data, response, error) in
guard let dataResponse = data else { return }
if let json = try! JSONSerialization.jsonObject(with: dataResponse, options:[]) as? [[String: Any]] {
for data in json {
let title = data["title"] as! String
let details = data["details"] as? String
guard let type = data["type"] as? Int else { return }
let valueType = self.valueType(dataType: type)
guard let coordinates = data["coordinates"] as? [String: Any] else { return }
self.adresse?.append(Adresse(title: title, details: details, type: valueType, coordinate: coordinates))
}
}
print(self.adresse)
}
self.TitleTableView.reloadData()
task.resume()
}
private func valueType(dataType: Int) -> Types {
if(dataType == 1) {
return Types.School
} else if (dataType == 2) {
return Types.Hospital
} else if (dataType == 3) {
return Types.Station_Essence
} else {
return Types.Restaurant
}
}
}
extension ListMapViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.adresse?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Adresse", for: indexPath) as! ListMapTableViewCell
if let adresse = self.adresse?[indexPath.row] {
cell.draw(adresse: adresse)
}
return cell
}
}
extension ListMapViewController: UITableViewDelegate {
}
You have two big problems.
self.adresse is nil. You never assign it a value. So all of the self.adresse?... do nothing.
You call reloadData too soon. It needs to be done inside the completion block, after you update the data. And it needs to be on the main queue.
To fix #1, change var adresse: [Adresse]? to var adresse = [Adresse](). Then you can get rid of all the ? after uses of adresse.
To fix #2, add:
DispatchQueue.main.async {
self.TitleTableView.reloadData()
}
just after the print at the end of the completion block. Don't forget to remove the current call to 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))

Search Bar with JSON Objects in TableView - Swift

class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource,UISearchBarDelegate,UISearchDisplayDelegate{
#IBOutlet weak var recipeTable: UITableView!
#IBOutlet weak var searchbarValue: UISearchBar!
// search functionality
var filteredAnswers: [JSON]?
func searchBarSearchButtonClicked(_ searchBar: UISearchBar){
self.filteredAnswers?.removeAll()
if (searchBar.text?.isEmpty)! {
self.filteredAnswers = self.recipes } else {
if self.recipes.count > 0 {
for i in 0...self.recipes.count - 1 {
let answer = self.recipes[i] as [Dictionary<String, AnyObject>]
if answer.title.range(of: searchBar.text!, options: .caseInsensitive) != nil {
self.filteredAnswers.append(answer)
}
}
}
}
recipeTable.reloadData();
recipeTable.reloadInputViews();
searchBar.resignFirstResponder()
}
//end search parameters
// tableview functionionalitys
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return recipes.count
}
// tableview functionalities
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! RecipeTableViewCell
cell.recipeLabel.text = recipes[indexPath.row].title
//cell.textLabel?.text = recipe.title
//cell.imageView?.image = recipe.imageUrl
return cell
}
// structs for json
struct Root : Decodable {
let count : Int
let recipes : [Recipe]
}
struct Recipe : Decodable { // It's highly recommended to declare Recipe in singular form
let recipeId : String
let imageUrl, sourceUrl, f2fUrl : URL
let title : String
let publisher : String
let socialRank : Double
let page : Int?
let ingredients : [String]?
}
//recipes is array of Recipes
var recipes = [Recipe]() // array of recipes
//unfiltered recipes to put into search
var filteredRecipes = [Recipe]()
fileprivate func getRecipes() {
let jsonURL = "http://food2fork.com/api/search?key=264045e3ff7b84ee346eb20e1642d9d9"
//.data(using: .utf8)!
//let somedata = Data(jsonURL.utf8)
guard let url = URL(string: jsonURL) else{return}
URLSession.shared.dataTask(with: url) {(data, response , err) in
if let response = response as? HTTPURLResponse, response.statusCode != 200 {
print(response.statusCode)
return
}
DispatchQueue.main.async {
if let err = err{
print("failed to get data from URL",err)
return
}
guard let data = data else{return}
//print(String(data: data, encoding: .utf8))
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result = try decoder.decode(Root.self, from: data)
self.recipes = result.recipes
//print(result.recipes)
self.recipeTable.reloadData()
}catch let jsonERR {
print("Failed to decode",jsonERR)
}
}
}.resume()
}
override func viewDidLoad() {
super.viewDidLoad()
//search functionalities
self.searchbarValue.delegate = self
//call json object
getRecipes()
}
}
I am trying to implement a search bar that takes ingredients from the JSON Object and shows the recipes that contain those ingredients in my table view. I am hoping for some best practices and help with this. I have tried a couple different strategies and none seem to be working.
This is the last one I have tried to implement, but I am getting errors in the search functionality.
self.recipes.count in searchBarSearchButtonClicked Cannot assign value
of type '[ViewController.Recipe]' to type '[JSON]?
But I'm also getting an assertion failure in -
[UISearchResultsTableView
_dequeueReusableCellWithIdentifier:forIndexPath:usingPresentationValues:]
I would like to get help but also improve and find the best way to do this. Thanks.
First of all your logic to filter the recipes cannot work and is very, very inefficient. It seems you copied and pasted the code from a completely unrelated source.
Basically the type of the data source array and the type of the filtered array must be the same, so you have to use filteredRecipes rather than filteredAnswers.
To filter the recipes with matching ingredients use filter and contains
func searchBarSearchButtonClicked(_ searchBar: UISearchBar){
filteredRecipes.removeAll()
if let searchText = searchBar.text, !searchText.isEmpty {
self.filteredRecipes = self.recipes.filter { recipe in
guard let ingredients = recipe.ingredients else { return false }
return ingredients.contains { $0.range(of: searchText, options: .caseInsensitive) != nil }
}
} else {
self.filteredRecipes = self.recipes
}
recipeTable.reloadData();
recipeTable.reloadInputViews();
searchBar.resignFirstResponder()
}
Actually this code is supposed to be executed in the delegate method
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String)
rather than in searchBarSearchButtonClicked
And – very important – you have to add a boolean property to indicate isSearching and in all related datasource and delegate methods you have to add a condition to show the data of filteredRecipes if isSearching is true.