Creating Tableview Sections from JSON Data Swift 4

Show JSON as TableView Section and TableView row in Swift 4.2 without Decodable?

I did above solutions using Alamofire 5.0 and below is my complete solutions so that it can help someone:

// Create a Modal Class of name "Event"

class Event {
var name: String?
var date: String?
var time: String?
var amOrPm: String?
var day: String?
var description: String?

init(dic: [String: Any]) {
if let name = dic["name"] as? String {
self.name = name
}
if let date = dic["date"] as? String {
self.date = date
}
if let time = dic["time"] as? String {
self.time = time
}
if let amOrPm = dic["am_or_pm"] as? String {
self.amOrPm = amOrPm
}
if let day = dic["day"] as? String {
self.day = day
}
if let description = dic["description"] as? String {
self.description = description
}
}
}

// Creating a Global Class for URL

import Alamofire

class HttpManager {
struct Constants {
static let baseUrl = "https://my-json-server.typicode.com/JCkshone/jsonTest"
}

func getEvents(handledResponse: @escaping (_ data: [[String: Any]])->()) {
let request = AF.request("\(Constants.baseUrl)/db")

request.responseJSON { (response: AFDataResponse<Any>) in
guard let data = response.value as? [String: Any] else { return }
if let events: [[String: Any]] = data["events"] as? [[String: Any]] {
handledResponse(events)
}
}
}
}

// Finally ViewController.swift

struct SectionEvent {
var sectionName: String
var evenst: [Event]
}

class ViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
let http = HttpManager()
var events: [Event] = []
var tableViewData: [SectionEvent] = []
let cell = "cellId"

override func viewDidLoad() {
super.viewDidLoad()
fetchData()
initTableView()
}

func fetchData() {
http.getEvents { (data: [[String : Any]]) in
data.forEach { item in
self.events.append(Event(dic: item))
}
self.buildData()
}
}

func buildData() {
events.forEach { event in

let validation = validateEventExist(event: event)
if !validation.exist {
tableViewData.append(SectionEvent(sectionName: event.date ?? "", evenst: [event]))
} else {
tableViewData[validation.position].evenst.append(event)
}
}
self.tableView.reloadData()
}

func validateEventExist(event: Event) -> (exist: Bool, position: Int) {
let filterData = tableViewData.filter {$0.sectionName == event.date}
let index = tableViewData.firstIndex { $0.sectionName == event.date}
return (filterData.count > 0, index ?? 0)
}

func initTableView() {
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cell)
tableView.tableHeaderView = UIView()
}
}

extension ViewController: UITableViewDataSource, UITableViewDelegate {

func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
tableViewData[section].sectionName
}

func numberOfSections(in tableView: UITableView) -> Int {
tableViewData.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
tableViewData[section].evenst.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath)
if let name = tableViewData[indexPath.section].evenst[indexPath.row].name, let description = tableViewData[indexPath.section].evenst[indexPath.row].description {
cell.textLabel?.text = "\(name) \n\(description)"
cell.textLabel?.numberOfLines = 0
}

return cell
}
}

How to Fetch JSON to Swift to tableView as Sections and Rows?

Like @vadian mentions tuples should be avoided for this so here is an improved solution.

Instead of a tuple we can use a struct to hold the grouped data

struct UsersByID {
let id: Int
var users : [User]
}

then change the load function to

func load(withUsers users: [User]) {
let dict = Dictionary(grouping: users) { return $0.userID }
usersByID = dict.map { (key, values) in
return UsersByID(id: key, users: values)
}.sorted(by: { $0.id < $1.id })
}

The rest of the code is the same but replace key with id and value with users


Old solution

First create a dictionary to hold your sections (keys) and rows (values) as a property in the view controller

var usersByID = [(key: Int, value: [User])]()

then fill that dictionary using grouping:by: using the array from json

func load(withUsers users: [User]) {
usersByID = Dictionary(grouping: users, by: { user in
user.userID }).sorted(by: { $0.0 < $1.0})
}

then the table view functions use this dictionary


override func numberOfSections(in tableView: UITableView) -> Int {
return usersByID.count
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return usersByID[section].value.count
}

override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return String(usersByID[section].key)
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "LabelCell", for: indexPath)

let user = usersByID[indexPath.section].value[indexPath.row]
cell.textLabel?.text = user.title
//...

return cell
}

Create table view section from a data model from a JSON file

I believe you need to index the data and then use the raw value (string) as ActionGoal is an enum.

override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return result?.data[section].actionGoal.rawValue
}

UITableView with Sections as Date from Json Data

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

Swift: How to configure a tableview with multiple sections dynamically using JSON data

You can build your data structure like that:

var teams = [[String: [Datum]]]()
for team in teams {
let teamProjects = projects.filter {$0.relationships.team.id == team.id}
let teamData = [team: [teamProjects]]
teams.append(teamData)
}

and in numberOfRows method:

 let team = teamsWithProjects[section-1]
let teamProject = Array(team.values)[0]
return teamProject.count

and in cellForRow method:

  let team = teamsWithProjects[indexPath.section-1]
let teamProject = Array(team.values)[0]
projectCell.textLabel?.text = teamProject[indexPath.row].attributes.name

Hope this will help you!

UITableView with sections from a local JSON file

Split your goal up into separate tasks and write a function for each.

You need to be able to:

  • Download your bonuses from the server
  • Save your bonuses to a local file
  • Load your bonuses from a local file

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()
}
}

How to load JSON array data into UITableView Section and Row using Swift?

To display the JSON in sections efficiently you have to decode the JSON into a struct with a title member

struct Root : Decodable {
let status : Bool
let sections : [Section]

private enum CodingKeys : String, CodingKey { case status, data }

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
status = try container.decode(Bool.self, forKey: .status)
let data = try container.decode([String:[Result]].self, forKey: .data)
sections = data.compactMap{ return $0.value.isEmpty ? nil : Section(title: $0.key, result: $0.value) }
}
}

struct Section {
let title : String
let result : [Result]
}

struct Result : Decodable {
let id, name, date : String
let group : [String]
}

Declare a data source array

var sections = [Section]()

Assign the result to the array

do {
let decoder = try JSONDecoder().decode(Root.self, from: data!)
let status = decoder.status

if status == true {
sections = decoder.sections
DispatchQueue.main.async {
self.tableView.reloadData()
}
} else {

}
} catch { print(error) }

The relevant table view data source methods are

override func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections[section].result.count
}

override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sections[section].title
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath)
let item = sections[indexPath.section].result[indexPath.row]
// Update the UI
}

Side note: Name your structs with more meaningful names. For example an array is supposed to be named with something in plural form (like in my previous suggestion)



Related Topics



Leave a reply



Submit