skip to Main Content

I have a stackView(vertical) which contains labels and bottom description label is hidden by default. And I implemented an arrow button at the right side of the cell. By clicking the button, I just want to show the hidden description label and stackView should expand automatically and make cell bigger. This was my basic idea to implement expandable cell.
So this is the code I used to get desired results:

@objc func downArrowButtonClicked(_ sender: UIButton){
        let indexPath = IndexPath(row: sender.tag, section: 0)
        selectedIndex = indexPath
        selectedCellIndex = sender.tag
        isDescHidden = !isDescHidden
        tableView.reloadRows(at: [indexPath], with: .automatic)
    }

Above is the code for the button inside clicked cell. I went with the idea to reload that particular index. I created a variable named selectedCellIndex of in which I use in cellForRowAt method to make some changes.

I also had the implement some code in viewDidLayoutSubviews() as when I first clicked the cell wasn’t getting expanded fully. here’s that just in case:

 override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        let indexPath = selectedIndex
        tableView.reloadRows(at: [indexPath ?? IndexPath(row: 0, section: 0)], with: .automatic)
    }

And calling it in willDisplay method which finally fixed the cell expansion issue:

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        viewDidLayoutSubviews()
    }

And here is my cellForRowAt function:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CustomTableViewCell", for: indexPath) as! CustomTableViewCell
            
            if indexPath.row == 0 {
                cell.lblTitle.text = "Title 1"
                cell.lblDesc.text = "Desc 1"
            }
            else if indexPath.row == 1 {
                cell.lblTitle.text = "Title 2"
                cell.lblDesc.text = "Desc 2"
            }
            else {
                cell.lblTitle.text = "Title 3"
                cell.lblDesc.text = "Desc 3"
            }
            
            if selectedCellIndex != nil {
                if isDescHidden == false {
                    if cell.isDescHidden == true {
                        cell.lblDesc.isHidden = false
                        cell.btnArrow.setImage(UIImage(systemName: "chevron.up"), for: .normal)
                    }
                    else {
                        cell.lblDesc.isHidden = true
                        cell.btnArrow.setImage(UIImage(systemName: "chevron.down"), for: .normal)
                    }
                }
                else {
                    if cell.isDescHidden == true {
                        cell.lblDesc.isHidden = true
                        cell.btnArrow.setImage(UIImage(systemName: "chevron.down"), for: .normal)
                    }
                    else {
                        cell.lblDesc.isHidden = false
                        cell.btnArrow.setImage(UIImage(systemName: "chevron.up"), for: .normal)
                    }
                }
                cell.isDescHidden = !cell.isDescHidden
            }
            
            cell.btnArrow.tag = indexPath.row
            cell.btnArrow.addTarget(self, action: #selector(downArrowButtonClicked(_:)), for: .touchUpInside)
            
            return cell
    }

This approach gets too confusing as you can see from the above code. The isDescHidden variable is defined in both Main view controller as well as table view cell class and I was trying to use both to expand or collapse a particular cell. However first time it works but if I have 3 cells expanded, collapsing button click doesn’t work for 1-2 clicks then works.

Is there a better approach for this kind of problem? Or is there any way I can directly set cell.isDescHidden value from @objc func downArrowButtonClicked(_ sender: UIButton) function? So I can use that in cellForRowAt function?
I would be glad if I could directly make changes to cell variables from that.

2

Answers


  1. Use the following function for automatic height for rows and provided top and bottom constraints to your stackView

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            return UITableView.automaticDimension
        }
    

    Here is my CustomCell

    class CustomCell: UITableViewCell {
        @IBOutlet weak var titleCell: UILabel!
        @IBOutlet weak var detail: UILabel!
        @IBOutlet weak var arrowButton: UIButton!
    
        let upArrow = UIImage(systemName: "chevron.up.circle.fill")
        let downArrow = UIImage(systemName: "chevron.down.circle.fill")
        var onArrowClick: ((UIButton)->())!
        
        @IBAction func handleArrowButton(sender: UIButton){
            onArrowClick(sender)
        }
    
        func updateArrowImage(expandStatus: Bool){
            arrowButton.setImage(expandStatus ? downArrow : upArrow, for: .normal)
        }
    }
    

    For sample Data

       let data = [
            ["Nothing", "description is very long description is very long description is very long description is very  "],
            ["Nothing", "description is very long "],
            ["Nothing", "description is very long "],
            ["Nothing", "description is very long "],
            ["Nothing", "description is very long "]
        ]
    
    var eachCellStatus: [Bool] = []
    
    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
         super.viewDidLoad()
         tableView.dataSource = self
         tableView.delegate = self
         for _ in data {
             eachCellStatus.append(true)
         }
    }
    

    TableView methods are like this

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return data.count
        }
        
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! CustomCell
            cell.titleCell.text = data[indexPath.row][0]
            cell.detail.text = data[indexPath.row][1]
            cell.detail.isHidden = eachCellStatus[indexPath.row]
            cell.updateArrowImage(expandStatus: self.eachCellStatus[indexPath.row])
            cell.onArrowClick = { button in
                self.eachCellStatus[indexPath.row].toggle()
                cell.updateArrowImage(expandStatus: self.eachCellStatus[indexPath.row])
                tableView.reloadRows(at: [indexPath], with: .automatic)
            }
            return cell
        }
    
    Login or Signup to reply.
  2. First create a model to show data into cell. You have to preserve the state of cell.

    struct CellData {
        var title: String
        var details: String
        var isExpanded: Bool
    }
    

    In CustomTableViewCell add a property for cellData and assign Outlets data from it. Also create a protocol to reload row from UIViewController

    protocol CustomTableViewCellDelegate {
        func reloadRow(sender: CustomTableViewCell, flag: Bool)
    }
    
    class CustomTableViewCell: UITableViewCell {
        
        @IBOutlet weak var titleLabel: UILabel!
        @IBOutlet weak var detailsLabel: UILabel!
        @IBOutlet weak var showButton: UIButton!
        
        var indexPath: IndexPath?
        var delegate: CustomTableViewCellDelegate?
        
        var data: CellData? {
            didSet {
                
                if let data = data {
                    if data.isExpanded == false {
                        detailsLabel.isHidden = true
                        showButton.setImage(UIImage(systemName: "chevron.down"), for: .normal)
                        
                    }else {
                        detailsLabel.isHidden = true
                        showButton.setImage(UIImage(systemName: "chevron.up"), for: .normal)
                    }
                    
                    titleLabel.text = data.title
                    detailsLabel.text = data.details
                }
            }
        }
        
        override func awakeFromNib() {
            super.awakeFromNib()
            titleLabel.text = nil
            detailsLabel.text = nil
        }
        
        @IBAction func showButtonAction(_ sender: UIButton) {
            if var data = data {
                data.isExpanded.toggle()
                delegate?.reloadRow(cell: self, flag: data.isExpanded)
            }
        }
    }
    

    In UIViewController add an array of CellData type. You may assign it’s data in viewDidLoad() method.

    var tableData: [CellData]
    

    Modify numberOfRowsInSection() and cellForRow() method like bleow.

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return tableData.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CustomTableViewCell", for: indexPath) as! CustomTableViewCell
        cell.data = tableData[indexPath.row]
        cell.delegate = self
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return UITableView.automaticDimension
    }
    

    Then confirm CustomTableViewCellDelegate protocol to UIViewController

    extension ViewController: CustomTableViewCellDelegate {
        func reloadRow(sender: CustomTableViewCell, isExpanded: Bool) {
            guard let tappedIndexPath = tableView.indexPath(for: sender) else { return }
            tableData[tappedIndexPath.row].isExpanded = isExpanded
            tableView.reloadRows(at: [indexPath], with: UITableView.RowAnimation.automatic)
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search