How to set up a expandable TableView Cell?

Hey guys, I want to create a expandable Cell in my tableView but there are a few things which don't work. This is my Code:

// In TableView Cell
Code Block
var bottomViewIsVisible = false
func setUpArrowButton() {
        arrowButton.tintColor = .lightGray
        arrowButton.setImage(downImage, for: .normal)
        if bottomViewIsVisible == false {
        arrowButton.addTarget(self, action: #selector(expandView), for: .touchUpInside)
        }
        if bottomViewIsVisible == true {
            arrowButton.addTarget(self, action: #selector(self.shrinkView), for: .touchUpInside)
        }
        
        //Constraints
        arrowButton.translatesAutoresizingMaskIntoConstraints = false
        arrowButton.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
        arrowButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -15).isActive = true
        arrowButton.widthAnchor.constraint(equalToConstant: 40).isActive = true
        arrowButton.heightAnchor.constraint(equalToConstant: 40).isActive = true
    }
    
    @objc func expandView() {
        bottomViewIsVisible = true
        UIButton.animate(withDuration: 0.3) {
            self.bottomView.isHidden = false
            self.arrowButton.setImage(self.upImage, for: .normal)
                    }
    }
    
    @objc func shrinkView() {
        bottomViewIsVisible = false
        UIButton.animate(withDuration: 0.3) {
            self.bottomView.isHidden = true
            self.arrowButton.setImage(self.downImage, for: .normal)
    }
    }
    
    func setUpBottomView() {
        bottomView.backgroundColor = .systemPink
        
        bottomView.translatesAutoresizingMaskIntoConstraints = false
        bottomView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        bottomView.topAnchor.constraint(equalTo: Label.bottomAnchor, constant: 5).isActive = true
        bottomView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10).isActive = true
        bottomView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10).isActive = true
        bottomView.heightAnchor.constraint(equalToConstant: 60).isActive = true
    }
    


In TableView:
Code Block
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell  {
if indexPath.row == 2 {
            let priceSliderCell = tableView.dequeueReusableCell(withIdentifier: "PriceSlider", for: indexPath) as! PriceRangeCell
            priceSliderCell.setUpTitleLabel(text: "Price")
            priceSliderCell.setUpArrowButton()
            priceSliderCell.setUpBottomView()
                tableView.beginUpdates()
                tableView.endUpdates()
                tableView.deselectRow(at: indexPath, animated: true)
            priceSliderCell.delegate = self
            priceSliderCell.selectionStyle = .none
            return priceSliderCell
        }
}


The first and biggest problem is that the cell height in the tableView doesn't fit to the height it should have after the cell expands. How can I do that?
The second problem is that the shrink functions doesn't work.

I hope someone can help me, thanks in advance!
You should define

Code Block
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return // Compute the CGFloat row height, depending on the cell
    }
  • store in an array the state of the cells (in fact it should be included in the dataSource):

Code Block
var visibleBottomViews : [Bool]
  • In viewDidLoad, you initialise it (all false for instance)

  • When you show/hide a cell, you change corresponding value of visibleBottomViews.

To do the computation, declare some global const for the class:
Code Block
let heightOfBottomView = CGFloat(30) // in fact the exact height of bottomView
let basicHeight = CGFloat(44) // In fact the value you need
let fullHeight = basicHeight + heightOfBottomView
  • use it in the computation by testing visibleBottomViews[indexPath.row]

Code Block
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return visibleBottomViews[indexPath.row] ? fullHeight : basicHeight
    }

Note: in cellForRowAt
Code Block
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell  {
if indexPath.row == 2 {

you only create cell for row == 2. What do you do for others ? You must define as well.

If that works, don't forget to close the thread on the correct answer. Otherwise explain what's the remaining problem.
Do you mean like this:
Code Block
let heightOfBottomView = CGFloat(60)
    let basicHeight = CGFloat(50)
    lazy var fullHeight = heightOfBottomView + basicHeight
    
    var visibleBottomViews : [Bool] = []
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        if indexPath.row == 0 {
            //SegmentControl Rent of Buy
            return 60
        }
        if indexPath.row == 1 {
            //Segment Control Property Types
            return 60
        }
        if indexPath.row == 2 {
            //Price Slider
            return visibleBottomViews[indexPath.row] ? fullHeight : basicHeight
        }
        if indexPath.row == 3 {
            //Size Slider
            return 118
}
        }


This throws me the following error:
Thread 1: Fatal error: Index out of range
Please, when you paste code, use Paste and Match Style to avoid extra lines:
Code Block
let heightOfBottomView = CGFloat(60)
let basicHeight = CGFloat(50)
lazy var fullHeight = heightOfBottomView + basicHeight
var visibleBottomViews : [Bool] = []
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == 0 {
//SegmentControl Rent of Buy
return 60
}
if indexPath.row == 1 {
//Segment Control Property Types
return 60
}
if indexPath.row == 2 {
//Price Slider
return visibleBottomViews[indexPath.row] ? fullHeight : basicHeight
}
if indexPath.row == 3 {
//Size Slider
return 118
}
}


Error is probably line 18.
Reason is that you have not populated visibleBottomViews array.

You should In ViewDidLoad:
Code Block
visibleBottomViews = Array(repeating: false, count: 4) // The exact count should be the table rows count


Note: I assumed all rows could have variable height. If variable size is only for row 2, you could replace the array by a single Bool
line 5:
Code Block
var visibleBottomView = false

and
Code Block
if indexPath.row == 2 {
//Price Slider
return visibleBottomView ? fullHeight : basicHeight
}



Okay well, I did everything as you told me but it is still not working:(
Is it possibile that you used a function for the tableView or tableView Cell I maybe don't have defined?

 it is still not working

This is not a precise information.
What do you get ?
Does it crash ?

Please post the code you have written, the complete code for the class.
Okay, no it does not crash, the bottom view is visible when I tap on the bottom but you can't really see him because the cell Height doesn't fit.
This is the implementation in the tableView:

Code Block
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell  {
if indexPath.row == 2 {
            let priceSliderCell = tableView.dequeueReusableCell(withIdentifier: "PriceSlider", for: indexPath) as! PriceRangeCell
            priceSliderCell.setUpTitleLabel(text: "Price")
            priceSliderCell.setUpArrowButton()
            priceSliderCell.setUpBottomView()
                tableView.beginUpdates()
                tableView.endUpdates()
                tableView.deselectRow(at: indexPath, animated: true)
            priceSliderCell.selectionStyle = .none
            return priceSliderCell
        }

This is the code for the cell Height:
Code Block
 //Cellheight IndexPath 2
    let heightOfBottomView = CGFloat(95)
    let basicHeight = CGFloat(50)
    lazy var fullHeight = heightOfBottomView + basicHeight
    var visibleBottomView = false
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
      //...
        if indexPath.row == 2 {
            return visibleBottomView ? fullHeight : basicHeight
        }


And this is the cell class:

Code Block
class PriceRangeCell: UITableViewCell {
    let Label = UILabel()
    let bottomView = UIView()
    var bottomViewIsVisible = false
    var delegate: ExpandProtocol?
    let arrowButton = UIButton()
    let arrowConf = UIImage.SymbolConfiguration(pointSize: 20)
    lazy var downImage = UIImage(systemName: "chevron.down", withConfiguration: arrowConf)
    lazy var upImage = UIImage(systemName: "chevron.up", withConfiguration: arrowConf)
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        contentView.addSubview(Label)
        contentView.addSubview(arrowButton)
        contentView.addSubview(bottomView)
        bottomView.isHidden = true
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setUpTitleLabel(text: String) {
        Label.text = text
        Label.textAlignment = .left
        Label.font = UIFont.systemFont(ofSize: 17, weight: .medium)
        Label.tintColor = .black
        Label.translatesAutoresizingMaskIntoConstraints = false
        Label.topAnchor.constraint(equalTo: topAnchor, constant: 10).isActive = true
        Label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 15).isActive = true
        Label.heightAnchor.constraint(equalToConstant: 30).isActive = true
        Label.widthAnchor.constraint(equalToConstant: 160).isActive = true
    }
    func setUpArrowButton() {
        arrowButton.tintColor = .lightGray
        arrowButton.setImage(downImage, for: .normal)
        arrowButton.addTarget(self, action: #selector(expandView), for: .touchUpInside)
        arrowButton.translatesAutoresizingMaskIntoConstraints = false
        arrowButton.topAnchor.constraint(equalTo: topAnchor, constant: 10).isActive = true
        arrowButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -15).isActive = true
        arrowButton.widthAnchor.constraint(equalToConstant: 40).isActive = true
        arrowButton.heightAnchor.constraint(equalToConstant: 40).isActive = true
    }
    @objc func expandView() {
        if bottomViewIsVisible != false {
            UIButton.animate(withDuration: 0.3) {
                self.bottomView.isHidden = true
                self.arrowButton.setImage(self.downImage, for: .normal)
            }
            bottomViewIsVisible = false
        } else {
        UIButton.animate(withDuration: 0.3) {
            self.bottomView.isHidden = false
            self.arrowButton.setImage(self.upImage, for: .normal)
                    }
            bottomViewIsVisible = true
    }
    }
    func setUpBottomView() {
        bottomView.backgroundColor = .systemPink
        bottomView.translatesAutoresizingMaskIntoConstraints = false
        bottomView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        bottomView.topAnchor.constraint(equalTo: Label.bottomAnchor, constant: 5).isActive = true
        bottomView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10).isActive = true
        bottomView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10).isActive = true
        bottomView.heightAnchor.constraint(equalToConstant: 60).isActive = true
    }
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }
    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
        // Configure the view for the selected state
    }
}



I'm really thankful for your efforts!



I did not go into the detail of the code, but I suspect constraints.

Vertical constraints could force the height of the cell.
Pattern is following:
  • obj A has a constraint to the top of cell and an height constraint

  • obj B has a vertical spacing to A and a height constraint

  • obj C has vertical spacing to B and constraint to the bottom of cell

At the end, the cell height is forced to the sum of those values.
How to set up a expandable TableView Cell?
 
 
Q