I am facing an issue with loading data into UITableView
sections dynamically. My business requirement is I have a ViewController
named "Courses", in this view I have a tableView
with different sections for which I used TableViewHeaderFooterView
, for each header it will have it's related course name, number of chapters in that course and number of assignments for that course and I am getting all this data from an API call I am able to populate the tableView
headers with this data and also I will get an 'id' for each course which I added as a tag for each header. Now when I tap on the any of the header I have to make another API call by sending the tag value of the header which is courseID, so I will get the datasource for the tableView and it should expand the section with show the rows and data in the rows coming from the datasource.
I am able to do all this with the static data where I have the datasource before tapping the header, But I don't know how to do this if the data should is to be added dynamically on tapping the headers.
I have tried to do this, when I click on any of the header for the first time it is showing data for that section, but if I click on the same header again or any other header I am getting a crash with
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (0) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).
I am posting My models and code here:
Model for course names:
struct CourseNamesModel {
var courseName: String!
var courseNameLetter: String!
var numberOfChaptersAndAssignments: String!
var chapterCount: Int!
var courseId: Int!
var opened: Bool!
init(courseName: String, courseNameLetter: String, numberOfChaptersAndAssignments: String, chapterCount: Int, courseId: Int ,opened: Bool) {
self.courseName = courseName
self.courseNameLetter = courseNameLetter
self.numberOfChaptersAndAssignments = numberOfChaptersAndAssignments
self.chapterCount = chapterCount
self.courseId = courseId
self.opened = opened
}
}
Model for data after tapping the header:
struct CourseDataModel {
var chapterName: String!
var documentAndAssignmentCount: String!
init(chapterName: String, documentAndAssignmentCount: String!) {
self.chapterName = chapterName
self.documentAndAssignmentCount = documentAndAssignmentCount
}
}
Code for my viewController and TableView
import UIKit
import Alamofire
class CoursesViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, ExpandableHeaderViewDelegate {
@IBOutlet weak var tableView: UITableView!
var sectionData = [CourseNamesModel]()
var tableData = [CourseDataModel]()
var selectedIdexPath: IndexPath!
override func viewDidLoad() {
super.viewDidLoad()
self.setFontFamilyAndSize()
self.title = "Courses"
selectedIdexPath = IndexPath(row: -1, section: -1)
tableView.register(UINib(nibName: "ExpandableHeaderView", bundle: nil), forHeaderFooterViewReuseIdentifier: "expandableHeaderView")
getCourseNames()
}
func numberOfSections(in tableView: UITableView) -> Int {
return sectionData.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return tableView.frame.size.height/8.2
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if sectionData[indexPath.section].opened {
return tableView.frame.size.height/8.48275862069
} else {
return 0
}
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 1
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "expandableHeaderView") as! ExpandableHeaderView
headerView.customInit(courseName: sectionData[section].courseName, letterSign: sectionData[section].courseNameLetter, numberOfChaptersAndAssignments: sectionData[section].numberOfChaptersAndAssignments, section: section, delegate: self)
headerView.tag = sectionData[section].courseId
return headerView
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "dataCell") as! DataCell
cell.chapterName.text = tableData[indexPath.row].chapterName
cell.numberOfDocumentsAndAssignments.text = tableData[indexPath.row].documentAndAssignmentCount
return cell
}
func getCourseNames() {
sectionData = []
let courseNamesURL = "\(WebAPI.baseURL2 + WebAPI.coursesAPI)"
Alamofire.request(courseNamesURL, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: nil).responseJSON { response in
switch response.result {
case .success:
let responseData = response.result.value as? [[String: Any]]
guard let courseNamesData = responseData else {return}
for courseDetail in courseNamesData {
let courseName = courseDetail["CourseName"] as! String
let courseNameLetter = String(courseName.first!)
let chaptersCount = courseDetail["Chapterscount"] as! Int
let assignmentsCount = courseDetail["AssignmentCount"] as! Int
let chaptersAndAssignemntsCount = "\(chaptersCount) Chapters, \(assignmentsCount) Assignments"
let courseId = courseDetail["CourseId"] as! Int
self.sectionData.append(CourseNamesModel(courseName: courseName, courseNameLetter: courseNameLetter, numberOfChaptersAndAssignments: chaptersAndAssignemntsCount, chapterCount: chaptersCount, courseId: courseId, opened: false))
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
Code for the toggleSection(expand/Collapse) delegate fucntion:
func toggleSection(header: ExpandableHeaderView, section: Int) {
sectionData[section].opened = !sectionData[section].opened
tableData = []
let courseChaptersURL = "\(WebAPI.baseURL2 + WebAPI.courseChaptersAPI)\(header.tag)"
Alamofire.request(courseChaptersURL, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: nil).responseJSON {response in
switch response.result {
case .success:
let responseData = response.result.value as? [[String : Any]]
guard let courseChaptersData = responseData else {return}
for chapterDetail in courseChaptersData {
let chapterName = chapterDetail["ChapterName"] as! String
let documentsCount = chapterDetail["Documentscount"] as! Int
let assignmentsCount = chapterDetail["AssignmentCount"] as! Int
let documentsAndAssignmentsCount = "\(documentsCount) Documents, \(assignmentsCount) Assignments"
// let isMaterialPathDelete = chapterDetail["IsDeleteMaterialPath"] as! Bool
self.tableData.append(CourseDataModel(chapterName: chapterName, documentAndAssignmentCount: documentsAndAssignmentsCount))
}
print(self.tableData.count)
case .failure(let error):
print(error.localizedDescription)
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
tableView.beginUpdates()
tableView.endUpdates()
print("Selected Section Index is : \(section)")
}
This is all I have, I have been trying this for past 2 days I am not able to figure it out.
You should redesign your model. Model must be reflective of you UI. As section contains N number of rows so your model of section should have an
Array
ofRow Model
. So you could easily file the list of rows for particularSection
. ManagingSection & Row
in two differentArray
is a headache to manage.For Example.
Now in your
TableViewDataSource
methods use below approach.