Search code examples
iosswift

expand and contract label not working in UITableViewCell


I am trying to use a UITableView and have cell contents which will expand or contract when the user clicks on the label.

However, the behavior I'm seeing is that the cell will contract (e.g. I am changing the label's numberOfLines from 0 to 1, and then the label will contract). However, when I change the label's numberOfLines from 1 to 0 it doesn't expand.

I put together a simple test program to show the issue I'm having.

I'm using a UITapGestureRecognizer to handle the tap event for the label, and that is where I expand or contract the label. Does anyone have any idea what I'm doing wrong?

Here's my storyboard and view controller code.

enter image description here

import UIKit

class MyCell : UITableViewCell {

    @IBOutlet weak var myLabel: UILabel!
}

class TableViewController: UITableViewController {

    let cellID = "cell"

    override func viewDidLoad() {
        super.viewDidLoad()

        self.tableView.rowHeight = UITableViewAutomaticDimension
        self.tableView.estimatedRowHeight = 75

    }

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        // #warning Incomplete implementation, return the number of sections
        return 12
    }

    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return "section " + String(section)
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // #warning Incomplete implementation, return the number of rows
        return 4
    }

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

        cell.myLabel.isUserInteractionEnabled = true
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.handleCellTapped(_:)))
        cell.myLabel!.addGestureRecognizer(tapGesture)

        // Configure the cell...
        if indexPath.row % 2 == 0 {
            cell.myLabel?.numberOfLines = 1
            cell.myLabel.text = "This is some long text that should be truncated.  It should not span over multiple lines. Let's hope this actually works. \(indexPath.row)"
        } else {
            cell.myLabel?.numberOfLines = 0
            cell.myLabel.text = "This is some really, really long text.  It should span over multiple lines. Let's hope this actually works. \(indexPath.row)"
        }

        return cell
    }

    @objc func handleCellTapped(_ sender: UITapGestureRecognizer) {
        print("Inside handleCellTapped...")
        guard let label = (sender.view as? UILabel) else { return }

        //label.translatesAutoresizingMaskIntoConstraints = false

        // expand or contract the cell accordingly
        if label.numberOfLines == 0 {
            label.numberOfLines = 1
        } else {
            label.numberOfLines = 0
        }
    }
}

Solution

  • You almost get it, but here is a couple of things you should care about.

    First, handle the label by UIGestureRecognizer it's quite overhead. For that purposes UITableViewDelegate has own method:

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

    Second, you're using self-sizing cell, because of

    self.tableView.rowHeight = UITableViewAutomaticDimension
    self.tableView.estimatedRowHeight = 75
    

    There is one important rule for that: you should pin myLabel to each side of superview (see official docs why):

    enter image description here

    Last step, when the numberOfLines changed, you should animate cell's height (expand or collapse) without reloading the cell:

    tableView.beginUpdates()
    tableView.endUpdates()
    

    Docs:

    You can also use this method followed by the endUpdates() method to animate the change in the row heights without reloading the cell.


    Full code:

    class MyCell: UITableViewCell {
        @IBOutlet weak var myLabel: UILabel!
    }
    
    class TableViewController: UITableViewController {
    
        let cellID = "cell"
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            self.tableView.rowHeight = UITableViewAutomaticDimension
            self.tableView.estimatedRowHeight = 75
        }
    
        // MARK: - Table view data source
    
        override func numberOfSections(in tableView: UITableView) -> Int {
            return 12
        }
    
        override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
            return "section " + String(section)
        }
    
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return 4
        }
    
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: self.cellID, for: indexPath) as! MyCell
            cell.selectionStyle = .none // remove if you need cell selection
    
            if indexPath.row % 2 == 0 {
                cell.myLabel?.numberOfLines = 1
                cell.myLabel.text = "This is some long text that should be truncated.  It should not span over multiple lines. Let's hope this actually works. \(indexPath.row)"
            } else {
                cell.myLabel?.numberOfLines = 0
                cell.myLabel.text = "This is some really, really long text.  It should span over multiple lines. Let's hope this actually works. \(indexPath.row)"
            }
    
            return cell
        }
    
        override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            tableView.deselectRow(at: indexPath, animated: false)
    
            guard let cell = tableView.cellForRow(at: indexPath) as? MyCell else { return }
    
            cell.myLabel.numberOfLines = cell.myLabel.numberOfLines == 0 ? 1 : 0
    
            tableView.beginUpdates()
            tableView.endUpdates()
        }
    
    }