Swift - tableView Row height updates only after scrolling or toggle expand/collapse
Asked Answered
P

4

6

I am using CollapsibleTableView from here and modified it as per my requirement to achieve collapsible sections. Here is how it looks now.

Since there is a border for my section as per the UI design, I had chosen the section header to be my UI element that holds data in both collapsed and expanded modes.

Reason: I tried but couldn't get it working in this model explained below -

** Have my header elements in section header and details of each item in its cell. By default, the section is in collapsed state. When user taps on the header, the cell is toggled to display. As I said, since there is a border that needs to be shown to the whole section (tapped header and its cell), I chose section header to be my UI element of operation. Here is my code for tableView -

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return sections.count 
    }

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        switch indexPath.row {
        case 0:
            return sections[indexPath.section].collapsed! ? 0 : (100.0 + heightOfLabel2!)
        case 1:
            return 0
        case 2:
            return 0
        default:
            return 0
        }
    }


func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {

        let header = self.tableView.dequeueReusableHeaderFooterViewWithIdentifier("header") as! CollapsibleTableViewHeader

        if sections.count == 0 {
            self.tableView.userInteractionEnabled = false
            header.cornerRadiusView.layer.borderWidth = 0.0
            header.benefitAlertImage.hidden = true
            header.benefitAlertText.hidden = true
            header.amountLabel.hidden = true
            header.titleLabel.text = "No_Vouchers".localized()
        }
        else {
            header.amountLabel.hidden = false
            header.cornerRadiusView.layer.borderWidth = 1.0
            self.tableView.userInteractionEnabled = true
            header.titleLabel.text = sections[section].name
            header.arrowImage.image = UIImage(named: "voucherDownArrow")
            header.setCollapsed(sections[section].collapsed)

            let stringRepresentation = sections[section].items.joinWithSeparator(", ")

            header.benefitDetailText1.text = stringRepresentation
            header.benefitDetailText2.text = sections[section].shortDesc
            header.benefitDetailText3.text = sections[section].untilDate

            header.section = section
            header.delegate = self

            if sections[section].collapsed == true {
                header.benefitAlertImage.hidden = true
                header.benefitAlertText.hidden = true
            }
            else {
                if sections[section].isNearExpiration == true {
                    header.benefitAlertImage.hidden = false
                    header.benefitAlertText.hidden = false
                }
                else {
                    header.benefitAlertImage.hidden = true
                    header.benefitAlertText.hidden = true
                }
            }

            if appLanguageDefault == "nl" {
                self.totalAmountLabel.text = "€ \(sections[section].totalAvailableBudget)"
            }
            else {
                self.totalAmountLabel.text = "\(sections[section].totalAvailableBudget) €"
            }
        }

        return header
    }

Function to toggle collapse/expand - I am using height values of the "dynamically changing" UILabels inside the section and then using those values to extend the border (using its layoutconstraint).

func toggleSection(header: CollapsibleTableViewHeader, section: Int) {
        let collapsed = !sections[section].collapsed

        header.benefitAlertImage.hidden = true
        header.benefitAlertText.hidden = true
        // Toggle collapse
        sections[section].collapsed = collapsed
        header.setCollapsed(collapsed)

        // Toggle Alert Labels show and hide
        if sections[section].collapsed == true {
            header.cornerRadiusViewBtmConstraint.constant = 0.0
            header.cornerRadiusViewTopConstraint.constant = 20.0
            header.benefitAlertImage.hidden = true
            header.benefitAlertText.hidden = true
        }
        else {

            heightOfLabel2 = header.benefitDetailText2.bounds.size.height

            if sections[section].isNearExpiration == true {
                header.benefitAlertImage.hidden = false
                header.benefitAlertText.hidden = false
                header.cornerRadiusViewBtmConstraint.constant = -100.0 - heightOfLabel2!
                header.cornerRadiusViewTopConstraint.constant = 10.0
                if let noOfDays = sections[section].daysUntilExpiration {
                    if appLanguageDefault == "nl" {

                        header.benefitAlertText.text = "(nog \(noOfDays) dagen geldig)"
                    }
                    else {
                        header.benefitAlertText.text = "(valable encore \(noOfDays) jour(s))"
                    }
                }                
            }
            else {
                header.cornerRadiusViewBtmConstraint.constant = -80.0 - heightOfLabel2!
                header.cornerRadiusViewTopConstraint.constant = 20.0
                header.benefitAlertImage.hidden = true
                header.benefitAlertText.hidden = true
            }
        }

        // Adjust the height of the rows inside the section
        tableView.beginUpdates()
        for i in 0 ..< sections.count {
            tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: i, inSection: section)], withRowAnimation: .Automatic)
        }
        tableView.endUpdates()
    }

The problem: I need to have, few section headers in this table view to be expanded by default on the first launch of the view, based on some conditions. As I am calculating the height of the labels and using the heights to set for the border's top and bottom constraint, it has become difficult to show the expanded section header as per design.

The content comes out of the border since the height of my UILabel is being taken as 21 by default.

UPDATE: The row height changes only after I scroll through the view or when I toggle between collapse/expand

The Question: How do I calculate the heights of the UILabels present in my Section header by the first time launch of the view? (That means, after my REST call is done, data is fetched and then I need to get the UIlabel height).

Currently, I am using heightOfLabel2 = header.benefitDetailText2.bounds.size.height

(Or)

Is there a better way to achieve this?

Thanks in advance!

Priestcraft answered 23/6, 2017 at 10:0 Comment(1)
reloadData will help you ,Pseudocarp
S
-1

You can try this for String extension to calculate bounding rect

extension String {
    func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
        let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)

        return boundingBox.height
    }
}

Source: Figure out size of UILabel based on String in Swift

Sop answered 23/6, 2017 at 10:7 Comment(8)
Will this work when my view is being presented for the first time? as in my question? In fact, I tried this before but there is a problem with greatestFiniteMagnitude which is no more supportedPriestcraft
You asked for calculating height of labels when you already have data with you.. so you can call this method over string and it will give you the height..Sop
But greatestFiniteMagnitude says "CGFloat has no member greatestFiniteMagnitude"Priestcraft
Which swift version you are using?Sop
I am using Swift 2.3Priestcraft
Then use CGFloat.MaxRescind
Now I get CGFloat has no member MaxPriestcraft
@KrishnaKumar: I edited the answer with minor changes, that worked for Swift 2.3. My bad that I missed these small changesPriestcraft
D
1

Here's what I got working based on my understanding of the overall goals of OP. If I'm misunderstanding, the following is still a working example. Full working project is also linked below.

Goals:

  • Dynamically sized TableViewCells that are also
  • Collapsable to show/hide additional details

I tried a number of different ways, this is the only one that I could get working.

Overview

Design makes use of the following:

  • custom TableViewCells
  • Autolayout
  • TableView Automatic Dimension

So if you're not familiar with those (especially Autolayout, might want to review that first.

Dynamic TableViewCells

Interface Builder

Lay out your a prototype cell. It's easiest to increase the row height size. Start simply with just a few elements to make sure you can get it working. (even though adding into Autolayout can be a pain). For example, simply stack two labels vertically, full width of the layout. Make the top label 1 line for the "title" and the second 0 lines for the "details"

Important: To configure Labels and Text Areas to grow to the size of their content, you must set Labels to have 0 lines and Text Areas to not be scrollable. Those are the triggers for fit to contents.

The most important thing is making sure there is a constraint for all four sides of every element. This is essential to get the Automatic Dimensioning working.

CollapsableCell

Next we make a very basic custom class for that table cell prototype. Connect the labels to outlets in the custom cell class. Ex:

class CollapsableCell: UITableViewCell {

  @IBOutlet weak var titleLabel: UILabel!
  @IBOutlet weak var detailLabel: UILabel!

}

Starting simply with two labels is easiest.

Also make sure that in Interface Builder you set the prototype cell class to CollapsableCell and you give it a reuse ID.

CollapsableCellViewController

On to the ViewController. First the standard things for custom TableViewCells:

  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return data.count
  }


  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCell(withIdentifier: "collapsableCell", for: indexPath) as! CollapsableCell
    let item = data[indexPath.row]

    cell.titleLabel?.text = item.title
    cell.detailLabel?.text = item.detail

    return cell
  }

We've added functions to return the number of rows and to return a cell for a given Row using our custom Cell. Hopefully all straightforward.

Now normally there would be one more function, TableView(heightForRowAt:), that would be required, but don't add that (or take it out if you have it). This is where Auto Dimension comes in. Add the following to viewDidLoad:

  override func viewDidLoad() {
    ...
    // settings for dynamic resizing table cells
    tableView.rowHeight = UITableViewAutomaticDimension
    tableView.estimatedRowHeight = 50
    ...
  }

At this point if you set up the detail label to be 0 lines as described above and run the project, you should get cells of different sizes based on the amount of text you're putting in that label. That Dynamic TableViewCells done.

Collapsable Cells

To add collapse/expand functionality, we can just build off the dynamic sizing we have working at this point. Add the following function to the ViewController:

  override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

    guard let cell = tableView.cellForRow(at: indexPath) as? CollapsableCell else { return }
    let item = data[indexPath.row]

    // update fields
    cell.detailLabel.text = self.isExpanded[indexPath.row] ? item.detail1 : ""

    // update table
    tableView.beginUpdates()
    tableView.endUpdates()

    // toggle hidden status
    isExpanded[indexPath.row] = !isExpanded[indexPath.row]

  }

Also add 'var isExpanded = Bool' to your ViewController to store the current expanded status for your rows (This could also be class variable in your custom TableViewCell).

Build and click on one of the rows, it should shrink down to only show the title label. And that's the basics.

Sample Project: A working sample project with a few more fields and a disclosure chevron image is available at github. This also includes a separate view with a demo of a Stackview dynamically resizing based on content.

A Few Notes:

  • This is all done in normal TableViewCells. I know the OP was using header cells, and while I can't think of a reason why that wouldn't work the same way, there's no need to do it that way.
  • Adding and removing a subView is the method I originally thought would work best and be most efficient since a view could be loaded from a nib, and even stored ready to be re-added. For some reason I couldn't get this to resize after the subViews were added. I can't think of a reason it wouldn't work, but here is a solution that does.
Duprey answered 30/6, 2017 at 7:14 Comment(1)
To limit the show/hide functionality to taps in the top portion of the expanded cell, the example on this post should work, but didn't try it. Checking y location to be in non-expanded part would be straight forward.Duprey
J
0

If I understood your question correctly, what you want to do is to resize your tableHeaderView when you call toggleSection.

Therefore what you need to do for your tableHeaderView to resize is this

// get the headerView
let headerView = self.tableView(self.tableView, viewForHeaderInSection: someSection)

// tell the view that it needs to refresh its layout
headerView?.setNeedsDisplay()

// reload the tableView
tableView.reloadData() 
/* or */ 
// beginUpdates, endUpdates

Basically what you would do is to place the above code snippet inside your function toggleSection(header: CollapsibleTableViewHeader, section: Int)

func toggleSection(header: CollapsibleTableViewHeader, section: Int) {
    ...

    // I'm not sure if this `header` variable is the headerView so I'll just add my code snippet at the bottom
    header.setNeedsDisplay() 


    /* code snippet start */
    // get the headerView
    let headerView = self.tableView(self.tableView, viewForHeaderInSection: someSection)

    // tell the view that it needs to refresh its layout
    headerView?.setNeedsDisplay()
    /* code snippet end */

    // reload the tableView
    // Adjust the height of the rows inside the section
    tableView.beginUpdates()


    // You do not need this
    for i in 0 ..< sections.count {
        tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: i, inSection: section)], withRowAnimation: .Automatic) 
    }
    // You do not need this


    tableView.endUpdates()
}

Explanation: A tableView's headerView/footerView does not update its layout even if you call reloadData() and beginUpdates,endUpdates. You need to tell the view that it needs to update first and then you refresh the tableView

Finally you also need to apply these two codes

func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat {
    return estimatedHeight
}

func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
    return UITableViewAutomaticDimension
}
Josselyn answered 30/6, 2017 at 6:59 Comment(8)
Thanks for the answer Zonily, I will give this a try in sometime.Priestcraft
Also, I am not just looking for what happens after I toggle but I actually need the heights of the tableView header in each section, so that the expanded row looks without any UI issuePriestcraft
If you used NSLayoutConstraints correctly on your tableHeaderView it will expand. By the way, when you mean expanded row do you mean that you want your tableViewCells to expand too. If you want that I might have to revise my answer a little bit to accomodate for that tooJosselyn
yes, I think the cell should also expand, otherwise the exapnded header is overlapping onto the next section. I was about to ask about the NSLayoutConstraints - what care should I take when I am adding constraints? I have 8 labels, two images, one UIView (with 9 images) inside itPriestcraft
Ah no, if the tableHeaderView expands, since you're using estimatedHeightForHeaderInSection now, the details inside it won't overlap with your tableViewCells anymore. For your UIControls you should constraint them with each other.Josselyn
Just if my question loses on clarity, the CollapsibleTableViewHeader is an XIB and I am using it as my viewForHeaderInSection.Priestcraft
If you learn how to use NSLayoutConstraints. Your life would be lightyears easier, since you won't have to calculate the sizes anymore like what you're currently doing right now.Josselyn
Let us continue this discussion in chat.Josselyn
F
0

In this method,

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        switch indexPath.row {
        case 0:
            return sections[indexPath.section].collapsed! ? 0 : (100.0 + heightOfLabel2!)
        case 1:
            return 0
        case 2:
            return 0
        default:
            return 0
        }
    }

instead of using heightOfLabel2, try implementing the following method to calculate heights specific to each cell(since we know the text to be filled, its font and label width, we can calculate the height of label),

func getHeightForBenefitDetailText2ForIndexPath(indexPath: NSIndexPath)->CGFloat

So your method should look like this,

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        switch indexPath.row {
        case 0:
            return sections[indexPath.section].collapsed! ? 0 : (100.0 + getHeightForBenefitDetailText2ForIndexPath(indexPath))
        case 1:
            return 0
        case 2:
            return 0
        default:
            return 0
        }
    }

And regarding your problem to expand few cells by for the very first time, make sure you set the collapsed property to true for those cells before reloading the table.

As a performance improvement, you can store the height value calculated for each expanded cell in a dictionary and return the value from the dictionary, to avoid the same calculation again and again.

Hope this helps you. If not, do share a sample project for more insight about your problem.

Fivepenny answered 4/7, 2017 at 4:34 Comment(0)
S
-1

You can try this for String extension to calculate bounding rect

extension String {
    func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
        let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)

        return boundingBox.height
    }
}

Source: Figure out size of UILabel based on String in Swift

Sop answered 23/6, 2017 at 10:7 Comment(8)
Will this work when my view is being presented for the first time? as in my question? In fact, I tried this before but there is a problem with greatestFiniteMagnitude which is no more supportedPriestcraft
You asked for calculating height of labels when you already have data with you.. so you can call this method over string and it will give you the height..Sop
But greatestFiniteMagnitude says "CGFloat has no member greatestFiniteMagnitude"Priestcraft
Which swift version you are using?Sop
I am using Swift 2.3Priestcraft
Then use CGFloat.MaxRescind
Now I get CGFloat has no member MaxPriestcraft
@KrishnaKumar: I edited the answer with minor changes, that worked for Swift 2.3. My bad that I missed these small changesPriestcraft

© 2022 - 2024 — McMap. All rights reserved.