Why do collapsing/expandable iOS UITableView rows disappear on interaction? - ios

I am new to iOS Development and I just implemented a simple expandable sections UITableView. I am not able to understand why some rows disappear and sometimes change position when the row heights are recalculated on tapping the section header. I went through all the already answered questions on this topic and have not been able to find the right solution.
Following is a scenario:
Launch the app:
Tap on the section header:
Section expands
All other headers disappear
Tap again
Section collapses
The headers continue to be blank
Scrolled to the bottom and back to the top
The positions of headers changed
Scrolled to the bottom and back to the top again
The positions of headers changed again with some cells still blank
Things I have already tried:
Wrapping reloadRowsAtIndexPaths in updates block (beginUpdates() and endUpdates())
Using reloadRowsAtIndexPaths with animation set to .none
Removing reloadRowsAtIndexPaths at all while keeping the updates block
Using reloadData() instead which actually works but I lose animation
Code:
Here is the link to the project repository.

You're using cells for the header. You shouldn't do that, you need a regular UIView there, or at least a cell that's not being dequeued like that. There's a few warnings when you run it that give that away. Usually just make a standalone xib with the view and then have a static method like this in your header class. Make sure you tie your outlets to the view itself, and NOT the owner:
static func view() -> HeaderView {
return Bundle.main.loadNibNamed("HeaderView", owner: nil, options: nil)![0] as! HeaderView
}
You're reloading the cells in the section that grows, but when you change the section that's grown you'd need to at least reload the former section for it to take the changes to it's cell's height. You can reload the section by index instead of individual rows in both cases

Ok as you ask, I am changing my answer according to you.
import UIKit
class MyTableViewController: UITableViewController {
let rows = 2
var categories = [Int](repeating: 0, count: 10)
struct Constants {
static let noSelectedSection = -1
}
var selectedSection: Int = Constants.noSelectedSection
func selectedChanged(to selected: Int?) {
let oldIndex = selectedSection;
if let s = selected {
if selectedSection != s {
selectedSection = s
} else {
selectedSection = Constants.noSelectedSection
}
tableView.beginUpdates()
if(oldIndex != -1){
tableView.reloadSections([oldIndex,s], with: .automatic)
}else{
tableView.reloadSections([s], with: .automatic)
}
tableView.endUpdates()
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return categories.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("reloading section \(section)")
return (selectedSection == section) ? rows : 0;//rows
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return tableView.rowHeight
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return tableView.rowHeight
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableCell(withIdentifier: "Header")
if let categoryCell = cell as? MyTableViewCell {
categoryCell.category = section + 1
let recognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture))
recognizer.numberOfTapsRequired = 1
recognizer.numberOfTouchesRequired = 1
categoryCell.contentView.tag = section;
categoryCell.contentView.addGestureRecognizer(recognizer)
}
return cell?.contentView
}
func handleTapGesture(recognizer: UITapGestureRecognizer) {
if let sindex = recognizer.view?.tag {
selectedChanged(to: sindex)
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Body", for: indexPath)
if let label = cell.viewWithTag(1) as? UILabel {
label.text = "Body \(indexPath.section + 1) - \(indexPath.row + 1)"
}
return cell
}
}
As you can see now I am just reloading a particular section instead of reloading the whole table.
also, I have removed gesture recognizer from the cell & put this into the main controller.

Related

tableView data gets reloaded every time I scroll it

So every time I scroll my tableView it reloads data which I find ridiculous since it makes no sense to reload data as it hasn't been changed.
So I setup my tableView as follows:
func numberOfSections(in tableView: UITableView) -> Int {
return self.numberOfElements
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 6
}
My cells are really custom and they require spacing between them. I couldn't add an extra View to my cell to fake that spacing because I have corner radius and it just ruins it. So I had to make each row = a section and set the spacing as a section height.
My cell has a dynamic height and can change it's height when I click "more" button, so the cell extends a little.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if self.segmentedControl.selectedSegmentIndex == 0 {
if self.isCellSelectedAt[indexPath.section] {
return self.fullCellHeight
} else {
return self.shortCellHeight
}
} else {
return 148
}
}
And here's how I setup my cell:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = UITableViewCell()
if self.segmentedControl.selectedSegmentIndex == 0 {
cell = tableView.dequeueReusableCell(withIdentifier: String.className(CurrentDocCell.self)) as! CurrentDocCell
(cell as! CurrentDocCell).delegate = self
(cell as! CurrentDocCell).ID = indexPath.section
} else {
cell = tableView.dequeueReusableCell(withIdentifier: String.className(PromissoryDocCell.self)) as! PromissoryDocCell
}
return cell
}
So I have a segmentedControl by switching which I can present either one cell of a certain height or the other one which is expandable.
In my viewDidLoad I have only these settings for tableView:
self.tableView.registerCellNib(CurrentDocCell.self)
self.tableView.registerCellNib(PromissoryDocCell.self)
And to expand the cell I have this delegate method:
func showDetails(at ID: Int) {
self.tableView.beginUpdates()
self.isCellSelectedAt[ID] = !self.isCellSelectedAt[ID]
self.tableView.endUpdates()
}
I set a breakpoint at cellForRowAt tableView method and it indeed gets called every time I scroll my tableView.
Any ideas? I feel like doing another approach to make cell spacing might fix this issue.
A UITableView only loads that part of its datasource which gets currently displayed. This dramatically increases the performance of the tableview, especially if the datasource contains thousands of records.
So it is the normal behaviour to reload the needed parts of the datasource when you scroll.

how to perform specific action at certain table view row in swift?

I have a table view cell. I make an app for a tenant in the apartment to report the defect of the room facility. if the defect has been repaired (status: Complete), data from server will give defect.status == 2 (if defect.status == 1, still on process to be repaired), and it will show YES and NO Button like the picture above.
I want if it still on the repairment process, the view that contains "Are You satisfied" label and Yes No Button will not appear. The expected result should be like the picture below
here is the code I use to remove that satisfied or not view
extension RequestDefectVC : UITableViewDataSource {
//MARK: Table View Delegate & Datasource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return listDefects.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "defectCell", for: indexPath) as! RequestDefectCell
let dataDefect = listDefects[indexPath.row]
cell.defectData = dataDefect
if dataDefect.status == 2 {
if let wantToRemoveView = cell.commentResponseView {
wantToRemoveView.removeFromSuperview()
}
}
return cell
}
}
but unfortunately, if that wantToRemoveView.removeFromSuperview() is triggered, it will remove all the view in all cell, even though the status is completed like picture below
I want that satisfied or not view appears if the status is complete, otherwise, it will be removed. how to do that ?
For your costumed cells are reused, removing views will cause uncertain effects. You don't actually need the specific view to be removed, only if it stays invisible.
if dataDefect.status == 2 {
if let wantToRemoveView = cell.commentResponseView {
wantToRemoveView.isHidden = true
}
} else {
if let wantToRemoveView = cell.commentResponseView {
wantToRemoveView.isHidden = false
}
}
Create a height constraint for that view and hook it as IBOutlet and control it's constant according to that in cellForRowAt
self.askViewH.constant = show ? 50 : 0
cell.layoutIfNeeded()
return cell
I expect you using automatic tableView cells
#Alexa289 One suggestion is that you can take heightConstraints of UIView. then create IBOutlet of your height constraints and make its constant 0 when you want to hide otherwise assign value to your static height.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "defectCell", for: indexPath) as! RequestDefectCell
let dataDefect = listDefects[indexPath.row]
cell.defectData = dataDefect
if dataDefect.status == 2 {
cell.viewSatisficationHeightConstraints.constant = 50
} else {
cell.viewSatisficationHeightConstraints.constant = 0
}
return cell
}
Second suggestion is that you can take label and button in view and embed stackview to your view(view contain label and button)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "defectCell", for: indexPath) as! RequestDefectCell
let dataDefect = listDefects[indexPath.row]
cell.defectData = dataDefect
if dataDefect.status == 2 {
cell.viewSatisfication.isHidden = false
} else {
cell.viewSatisfication.isHidden = true
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 40
}
you can read about UIStackView which makes hiding things easier. If you are not using stackview and hiding things the UI will not good as the space used by the hidden view will be still there. So better to use stackView when need to hide or show some view.
UIStackView : https://developer.apple.com/documentation/uikit/uistackview

TableView Display Issues - Swift

I have a table view in Swift with headers that expand or contract to show or hide cells, but for some reason there are extra row dividers in the middle of the page for no reason, and the label in the header is not showing up.
class ExpandableHeader: UITableViewHeaderFooterView {
var section: Int = 0
let expandLabel = UILabel()
}
func numberOfSections(in tableView: UITableView) -> Int {
// return number of section in table from data
return list.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// return number of rows in each section from data
return list[section].items.count + 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Create cell with the identifier which was set in the storyboard prototype cell
// set cell data/name from our data object
if indexPath.row < list[indexPath.section].items.count {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = list[indexPath.section].items[indexPath.row].name
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "custom", for: indexPath)
return cell
}
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
// create a header of type of our subclassed header with section number
let headerView = ExpandableHeader()
headerView.expandLabel.text = "+"
headerView.expandLabel.frame.size.height = 30
headerView.expandLabel.frame.size.width = 30
headerView.expandLabel.textAlignment = NSTextAlignment.center
headerView.addSubview(headerView.expandLabel)
headerView.expandLabel.frame.origin.x = view.frame.maxX - headerView.expandLabel.frame.width
// assign selected/current section number to the header object
headerView.section = section
// create Gesture Recognizer to add ability to select header to this cutsom header with an action
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(headerClicked(sender:)))
// add Gesture Recognizer to our header
headerView.addGestureRecognizer(tapGesture)
return headerView
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// check if row's section expanded parameter is set to true or false. set height of rows accordingly to hide or show them
if list[indexPath.section].expanded == true {
return 44
} else {
return 0
}
}
I added the relevant lines of code above. When you change the row height to 0, should the cell separator lines also be changed to height 0, or hidden?
Why is the header in each section not going all the way to the right side of the screen when the storyboard has it all the way to the right edge?
Is this also why the text label for the header is not being displayed? Because the right edge is getting cut off?
Sorry for the basic questions, I'm still getting the hang of this, so any suggestions would be much appreciated.
To hide extra row divider use below lines of code
let backgroundView = UIView(frame: CGRect.zero)
self.tableView.tableFooterView = backgroundView //instead of tableView give your tableView name

Can't see sections in table view

I am trying to make a table view with multiple sections, but for some (presumably dumb) reason only the first section is visible on my table view.
var labels = ["label1", "label2"]
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 2
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
var sectionHeader = ""
if section == 0 {
sectionHeader = "first section header"
} else if section == 1 {
sectionHeader = "second section header"
}
return sectionHeader
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! UITableViewCell
if indexPath.section == 0 {
cell.textLabel!.text = labels[0]
} else if indexPath.section == 1 {
cell.textLabel!.text = labels[1]
}
return cell
}
The header for the first section appears along with a cell with label text "label1" but the second section is no where to be found.
Is there some sort of setting I am not setting? What am I doing wrong here.
Your code looks good , Few things you can check
1> Tableview's frame is not large enough to fit 2 section and it's not scroll-able also.
2> heightForRow delegate method or viewForHeader or heightforHeader returns 0 or nil
3> Your constraints are breaking at runtime and that's why tableview is not visible properly (you need to check console at runtime)
You can start visual debugger of XCode at runtime and see if your table view is not hidden by some other component etc... (Debug View Hierarchy)

Return two different sections in my UITableView

I'm trying to return two seperate .counts in my UITableView. I kind of know the logic behind it but not using the syntax right. How can I do this? What I want it to do is fill my tableview up with the both .counts. I would also like to put in a label on top of the first section and second section. Anything would help!
Here is what I have so far but I'm only getting the first section of cells. Why isnt it displaying both .counts?
func tableView(tableView: UITableView, numberOfSectionsInTableView: Int) -> Int{
return 2;
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (section == 0){
return featSubNames.count
}
else{
return subCatNames.count
}
}
func tableView(tableView: UITableView!, viewForHeaderInSection section: Int) -> UIView! {
if (section == 0){
????
}
if (section == 1){
????
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as UITableViewCell
// Configure the cell
switch (indexPath.section) {
case 0:
cell.textLabel?.text = featSubName[indexPath.row]
cell.textLabel?.textColor = UIColor.whiteColor()
case 1:
cell.textLabel?.text = subCatNames[indexPath.row]
cell.textLabel?.textColor = UIColor.whiteColor()
default:
cell.textLabel?.text = "Other"
}
return cell
}
Your code seems to be right. To get the UIViews on the header of each section, you can return any object that inherits from a UIView(this doesn't mean that would be nice to). So, you can return a small container with a UIImageView and a UILabel, if you want this for example.
Your code for the viewHeader would be something like this:
func tableView(tableView: UITableView!, viewForHeaderInSection section: Int) -> UIView! {
// instantiate the view to be returned and placed correctly
let commonLabel = UILabel(frame: CGRectMake(0, 0, tableWidth, 30))
commonLabel.textColor = .blackColor()
commonLabel.textAlignment = .Center
// ... other settings in the properties
if section == 0 {
commonLabel.text = "section1"
// other settings for the first section
}
if section == 1 {
commonLabel.text = "section2"
// other settings ...
}
return commonLabel
}
Notes:
Be sure to set the container view's frame,for sectionHeaders the top view in the hierarchy don't accept auto layout.
Implementing the viewForHeaderInSection method is more accessible to custom UI's, you can use the TitleForHeaderInSection if you wish, but it's limited for more complex stuffs.
EDIT:
If you're using storyboards, you still can use this code, although it's not so elegant in terms of apps that use IB for the UI. For this, you might take a look at this link: How to Implement Custom Table View Section Headers and Footers with Storyboard
. This implements only the view using storyboards, but the delegate part must to be wrote.

Resources