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
It Is Possible to Know If a String Is Encoded in Base64
Xcode 6 Swift Wkwebview Keyboard Settings
Traverse View Controller Hierarchy in Swift
Swift: Memory Not Clearing When I Segue to Another View Controller, Recieving Memory Warning
Fft Calculating Incorrectly - Swift
Exc_Bad_Access in Parent Class Init() with Xcode 10.2
Swift iOS - Tag Collection View
iOS How to Correctly Handle Orientation When Capturing Video Using Avassetwriter
How to Get Multiple Buttons from a Single Tableviewcell
Swift Spritekit Playing Audio in the Background
How to Draw Text in Center to Uiimage
iOS Swift 2 Record Video Avcapturesession
String to Double in Xcode 6's Swift
Avplayer Not Full Screen in Landscape Mode Using iOS Swift
Handle Multiple File (Image) Uploads to Aws S3 Swift
How to Call Presentviewcontroller in Uiview Class