Swift - Tableview Row Height Updates Only After Scrolling or Toggle Expand/Collapse

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

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

Table view displays correct cell´s height after some scrolling

I fixed it by replacing

cell.layoutIfNeeded()
cell.updateConstraintsIfNeeded()
cell.layoutSubviews()

with

cell.layoutIfNeeded()
cell.layoutSubviews()
cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()

How to change table cell height (collapse and expand) according to its content by clicking?

There are various approaches to "expandable" cells - this one may work well for your design needs...

The common way to get self-sizing cells is by making sure you have a clean "top-to-bottom chain" of constraints:

enter image description here

With this layout, the orange view has an 8-pt constraint to the bottom of the black view (its superview).

To make this cell expandable / collapsible, we can add another 8-pt constraint, this time from the bottom of the blue view to the bottom of the black view.

Initially, we'll have constraint conflicts, because the bottom of the black view cannot be 8-pts from the blue view and 8-pts from the orange view at the same time.

So, we give them different priorities...

If we give "blue-bottom" constraint a Priority of .defaultHigh (750) and the "orange-bottom" constraint a Priority of .defaultLow (250), we're telling auto-layout to enforce the constraint with the higher priority and allow the lower priority constraint to break, and we get this:

enter image description here

The orange view is still there, but it is now outside the bounds of the black view, so we don't see it.


Here is a very simple example...

We configure the cell with two Bottom constraints - one from the bottom of the Title Label View and one from the bottom of the Description Label View.

We set high or low priority on each constraint, depending on whether we want the cell expanded or collapsed.

Tapping on a row will toggle its expanded state.

This is all done via code - no @IBOutlet or @IBAction connections - so just add a new UITableViewController and assign its class to TestTableViewController:

class MyExpandableCell: UITableViewCell {

let myImageView: UIImageView = {
let v = UIImageView()
v.backgroundColor = UIColor(red: 219.0 / 255.0, green: 59.0 / 255.0, blue: 38.0 / 255.0, alpha: 1.0)
v.contentMode = .scaleAspectFit
v.tintColor = .white
v.layer.cornerRadius = 16.0
return v
}()

let myTitleView: UIView = {
let v = UIView()
v.backgroundColor = UIColor(red: 68.0 / 255.0, green: 161.0 / 255.0, blue: 247.0 / 255.0, alpha: 1.0)
v.layer.cornerRadius = 16.0
return v
}()

let myDescView: UIView = {
let v = UIView()
v.backgroundColor = UIColor(red: 243.0 / 255.0, green: 176.0 / 255.0, blue: 61.0 / 255.0, alpha: 1.0)
v.layer.cornerRadius = 16.0
return v
}()

let myTitleLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
v.textAlignment = .center
v.textColor = .white
return v
}()

let myDescLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
v.textColor = .white
return v
}()

let myContainerView: UIView = {
let v = UIView()
v.clipsToBounds = true
v.backgroundColor = .black
return v
}()

var isExpanded: Bool = false {
didSet {
expandedConstraint.priority = isExpanded ? .defaultHigh : .defaultLow
collapsedConstraint.priority = isExpanded ? .defaultLow : .defaultHigh
}
}

var collapsedConstraint: NSLayoutConstraint!
var expandedConstraint: NSLayoutConstraint!

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}

func commonInit() -> Void {

[myImageView, myTitleView, myDescView, myTitleLabel, myDescLabel, myContainerView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}

myTitleView.addSubview(myTitleLabel)
myDescView.addSubview(myDescLabel)
myContainerView.addSubview(myTitleView)
myContainerView.addSubview(myDescView)
myContainerView.addSubview(myImageView)
contentView.addSubview(myContainerView)

let g = contentView.layoutMarginsGuide

expandedConstraint = myDescView.bottomAnchor.constraint(equalTo: myContainerView.bottomAnchor, constant: -8.0)
collapsedConstraint = myTitleView.bottomAnchor.constraint(equalTo: myContainerView.bottomAnchor, constant: -8.0)

expandedConstraint.priority = .defaultLow
collapsedConstraint.priority = .defaultHigh

NSLayoutConstraint.activate([

myTitleLabel.topAnchor.constraint(equalTo: myTitleView.topAnchor, constant: 12.0),
myTitleLabel.leadingAnchor.constraint(equalTo: myTitleView.leadingAnchor, constant: 8.0),
myTitleLabel.trailingAnchor.constraint(equalTo: myTitleView.trailingAnchor, constant: -8.0),
myTitleLabel.bottomAnchor.constraint(equalTo: myTitleView.bottomAnchor, constant: -12.0),

myDescLabel.topAnchor.constraint(equalTo: myDescView.topAnchor, constant: 12.0),
myDescLabel.leadingAnchor.constraint(equalTo: myDescView.leadingAnchor, constant: 8.0),
myDescLabel.trailingAnchor.constraint(equalTo: myDescView.trailingAnchor, constant: -8.0),
myDescLabel.bottomAnchor.constraint(equalTo: myDescView.bottomAnchor, constant: -12.0),

myImageView.topAnchor.constraint(equalTo: myContainerView.topAnchor, constant: 8.0),
myImageView.leadingAnchor.constraint(equalTo: myContainerView.leadingAnchor, constant: 8.0),
myImageView.trailingAnchor.constraint(equalTo: myContainerView.trailingAnchor, constant: -8.0),

myImageView.heightAnchor.constraint(equalToConstant: 80),

myTitleView.topAnchor.constraint(equalTo: myImageView.bottomAnchor, constant: 8.0),
myTitleView.leadingAnchor.constraint(equalTo: myContainerView.leadingAnchor, constant: 8.0),
myTitleView.trailingAnchor.constraint(equalTo: myContainerView.trailingAnchor, constant: -8.0),

myDescView.topAnchor.constraint(equalTo: myTitleView.bottomAnchor, constant: 8.0),
myDescView.leadingAnchor.constraint(equalTo: myContainerView.leadingAnchor, constant: 8.0),
myDescView.trailingAnchor.constraint(equalTo: myContainerView.trailingAnchor, constant: -8.0),

myContainerView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
myContainerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
myContainerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
myContainerView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),

expandedConstraint, collapsedConstraint,
])

}

}

class TestTableViewController: UITableViewController {

let myData: [[String]] = [
["Label", "A label can contain an arbitrary amount of text, but UILabel may shrink, wrap, or truncate the text, depending on the size of the bounding rectangle and properties you set. You can control the font, text color, alignment, highlighting, and shadowing of the text in the label."],
["Button", "You can set the title, image, and other appearance properties of a button. In addition, you can specify a different appearance for each button state."],
["Segmented Control", "The segments can represent single or multiple selection, or a list of commands.\n\nEach segment can display text or an image, but not both."],
["Text Field", "Displays a rounded rectangle that can contain editable text. When a user taps a text field, a keyboard appears; when a user taps Return in the keyboard, the keyboard disappears and the text field can handle the input in an application-specific way. UITextField supports overlay views to display additional information, such as a bookmarks icon. UITextField also provides a clear text control a user taps to erase the contents of the text field."],
["Slider", "UISlider displays a horizontal bar, called a track, that represents a range of values. The current value is shown by the position of an indicator, or thumb. A user selects a value by sliding the thumb along the track. You can customize the appearance of both the track and the thumb."],
["This cell has a TItle that will wrap onto multiple lines.", "Just to demonstrate that auto-layout is handling text wrapping in the title view."],
]

var rowState: [Bool] = [Bool]()

override func viewDidLoad() {
super.viewDidLoad()

// initialize rowState array to all False (not expanded
rowState = Array(repeating: false, count: myData.count)

tableView.register(MyExpandableCell.self, forCellReuseIdentifier: "cell")
}

override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}

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

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyExpandableCell

cell.myImageView.image = UIImage(systemName: "\(indexPath.row).circle")
cell.myTitleLabel.text = myData[indexPath.row][0]
cell.myDescLabel.text = myData[indexPath.row][1]
cell.isExpanded = rowState[indexPath.row]

cell.selectionStyle = .none

return cell
}

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let c = tableView.cellForRow(at: indexPath) as? MyExpandableCell else {
return
}
rowState[indexPath.row].toggle()
tableView.performBatchUpdates({
c.isExpanded = rowState[indexPath.row]
}, completion: nil)
}

}

Result:

enter image description here

and, after tapping and scrolling a bit:

enter image description here

iOS - Expandable and reordering TableView

I just posted an answer to expanding/collapsing table view cells last night on another question. Theres a working sample project linked to on github as well:

Dynamic Sizing and Collapsable TableViewCells

As for changing the order, that is usually done through entering "Edit mode" for a tableview controller. Editing mode includes support for insertion, deletion, and re-ordering controls. The functionality is mostly build-in, just needs a little code to set it up.

To enter that mode, make the following call.

self.tableView.setEditing(editing, animated: animated)

The TableViewCell is where you configure the editing options. In Interface builder, when you select a prototype cell, you can set the following in the inspector:

  • an accessory to display in editing mode
  • whether to indent when editing
  • whether to show re-order control

That last one is what you'll want to make sure is checked.

The Apple tableView.setEditing docs give a decent overview in the description.

Expand/collapse section in UITableView in iOS

You have to make your own custom header row and put that as the first row of each section. Subclassing the UITableView or the headers that are already there will be a pain. Based on the way they work now, I am not sure you can easily get actions out of them. You could set up a cell to LOOK like a header, and setup the tableView:didSelectRowAtIndexPath to manually expand or collapse the section it is in.

I'd store an array of booleans corresponding the the "expended" value of each of your sections. Then you could have the tableView:didSelectRowAtIndexPath on each of your custom header rows toggle this value and then reload that specific section.

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row == 0) {
///it's the first row of any section so it would be your custom section header

///put in your code to toggle your boolean value here
mybooleans[indexPath.section] = !mybooleans[indexPath.section];

///reload this section
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationFade];
}
}

Then set numberOfRowsInSection to check the mybooleans value and return 1 if the section isn't expanded, or 1+ the number of items in the section if it is expanded.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

if (mybooleans[section]) {
///we want the number of people plus the header cell
return [self numberOfPeopleInGroup:section] + 1;
} else {
///we just want the header cell
return 1;
}
}

Also, you will need to update cellForRowAtIndexPath to return a custom header cell for the first row in any section.



Related Topics



Leave a reply



Submit