简体   繁体   中英

How to add data dynamically to UITableView when tapped on section headers

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 have inconsistent data source for your table view. Every section should have its own var tableData = [CourseDataModel]() , so in nubmerOfRowsInSection you should have:

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

Always update your data source on the main queue. Do it right before calling reloadData , so it should look like:

DispatchQueue.main.async {
    self.sectionData.append...
    // or self.tableData.append...
    self.tableView.reloadData()
    // or reload section
}

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 of Row Model . So you could easily file the list of rows for particular Section . Managing Section & Row in two different Array is a headache to manage.

For Example.

struct SectionModel {
    var opened: Bool!
    var yourTableRowModels = [RowModel]()
}

struct RowModel {
   var someAttribute: String!
}

Now in your TableViewDataSource methods use below approach.

class YourViewController: UIViewController {
    var sections = [SectionModel]()

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

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return sections[section].yourTableRowModels.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let rowModel = sections[indexPath.section].yourTableRowModels[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: "dataCell") as! DataCell
        cell.chapterName.text = rowModel.someAttribute
        cell.numberOfDocumentsAndAssignments.text = rowModel.someAttribute
        return cell
    }
}

It's the number of rows which is same for all sections.

Try below code :

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if sectionData[section].opened {
        return tableData.count
    }
    return 0
}

and there is no need of :

tableView.beginUpdates()
tableView.endUpdates()

in toggleSection I guess.

EDIT

Try one more approach:

In fun toggleSection :

for courseNamesModel in sectionData {
    courseNamesModel.opened = false
}
sectionData[section].opened = !sectionData[section].opened

as you have to set false to previously opened headers.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM