I have a tableviewcell that I want to expand and collapse on tap. All the examples I have found are Storyboard base and I am trying to do this programmatically. What I thought initially was to create a subview and constrain it to the content view but when I adjust the height of the cell with heightForRowAt
it also increases the size of the content view (which makes sense). Any thoughts on how I should go about this?
3 Answers
Use vertical UIStackView
with the bottom view isHidden
set to true, then on tap (or whatever is the trigger of the expand) just change the isHidden = false
. I guess that would be the easiest, considering how UIStackView
deals with isHidden
. Another approach is to setup autolayout constraints, and change height anchor of the bottom view by setting NSLayoutConstraint
's constant to 0.
Anyway, whichever of the appraoch will you choose, you will have to tell the tableView to refresh its display (from the viewcontroller):
func refreshTableAfterCellExpansion() {
self.tableView.beginUpdates()
self.tableView.setNeedsDisplay()
self.tableView.endUpdates()
}
E.g., check following SO question and its answer.
An example using playgrounds (the one with the UIStackView
, the other one uses the same principle):
import UIKit
import PlaygroundSupport
class ExpandableCellViewController: UITableViewController, ExpandableCellDelegate {
override func loadView() {
super.loadView()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44
tableView.register(ExpandableCell.self, forCellReuseIdentifier: "expandableCell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "expandableCell", for: indexPath) as! ExpandableCell
cell.delegate = self
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? ExpandableCell {
cell.isExpanded = true
}
}
override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? ExpandableCell {
cell.isExpanded = false
}
}
func expandableCellLayoutChanged(_ expandableCell: ExpandableCell) {
refreshTableAfterCellExpansion()
}
func refreshTableAfterCellExpansion() {
self.tableView.beginUpdates()
self.tableView.setNeedsDisplay()
self.tableView.endUpdates()
}
}
protocol ExpandableCellDelegate: class {
func expandableCellLayoutChanged(_ expandableCell: ExpandableCell)
}
class ExpandableCell: UITableViewCell {
weak var delegate: ExpandableCellDelegate?
fileprivate let stack = UIStackView()
fileprivate let topView = UIView()
fileprivate let bottomView = UIView()
var isExpanded: Bool = false {
didSet {
bottomView.isHidden = !isExpanded
delegate?.expandableCellLayoutChanged(self)
}
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
contentView.addSubview(stack)
stack.addArrangedSubview(topView)
stack.addArrangedSubview(bottomView)
stack.translatesAutoresizingMaskIntoConstraints = false
topView.translatesAutoresizingMaskIntoConstraints = false
bottomView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: contentView.topAnchor),
stack.leftAnchor.constraint(equalTo: contentView.leftAnchor),
stack.rightAnchor.constraint(equalTo: contentView.rightAnchor),
stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
topView.heightAnchor.constraint(equalToConstant: 50),
bottomView.heightAnchor.constraint(equalToConstant: 30),
])
stack.axis = .vertical
stack.distribution = .fill
stack.alignment = .fill
stack.spacing = 0
topView.backgroundColor = .red
bottomView.backgroundColor = .blue
bottomView.isHidden = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = ExpandableCellViewController()

- 19,169
- 4
- 55
- 90
-
Ah okay, I’ve never used UIStackView before, I’ll have to take a look – sanch Dec 24 '17 at 20:05
-
@MattSanford I have added a playgrounds example that you can use as a starting point – Milan Nosáľ Dec 24 '17 at 20:21
-
I keep running into the height constraint issue on the content view when the elements go from hidden to not. – sanch Dec 25 '17 at 18:22
-
This is the solution and it works correctly, I would just like to get rid of the errors in the console – sanch Dec 25 '17 at 20:21
-
what errors? unsatisfiable constraints? if that's the case and one of them is `UIView-Encapsulated-Layout-Height` or `UIView-Encapsulated-Layout-Width`, go see my answer to this question (that is a known issue with the dynamic height cells): https://stackoverflow.com/a/47937781/2912282 – Milan Nosáľ Dec 25 '17 at 21:41
I would simply define 2 cell types, and select the one which is appropriate in tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)

- 874
- 6
- 8
-
Are you saying that I would remove the “collapsed” Version and add insert the expanded row? I’ve read there was performance troubles associated with that – sanch Dec 24 '17 at 19:58
-
The idea would be to implement it in tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath). Call dequeueReusableCell with the Identifier corresponding to collapsed or expanded, for the given row. I've not looked at performance point of view. – claude31 Dec 24 '17 at 20:14
You can use a flag to indicate if specific section or row is expanded and use this methods on UITableView:
reloadRows()
reloadSections()
And Change heightForRow method to return desired height according to the flag. If you want to be able to expand more than one row at a time you can use a ViewModel for each item in your datasource and update a flag in it when didSelectItemAt or didDeselectItemAt methods called and then reload row or section with the desired animation.

- 1
- 2
-
I tried this, but the issue was that I’m not sure of where to attach subviews so that it displays appropriately – sanch Dec 24 '17 at 21:59
-
If you use reloadRows or reloadSections this will call cellForRow method you can adjust views there, you might want to add the views always but with 0 height constraint and then change the views height constraints in cellForRow method. – MohammadN Dec 24 '17 at 22:04