简体   繁体   中英

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

Initially the user sees cell like this (Only black area. Description is hidden). That is, a cell is visible up to description.

在此处输入图像描述

I want after clicking on the cell that its height increase to the end of the cell, like this.

在此处输入图像描述

Both Title and Description are not static. Their size depends on the content.

You can see in this case I always change the height to constant values. It's not good for my requirements.

extension MyTableView: UITableViewDataSource, UITableViewDelegate {

    //another funcs...

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return myDataArray.count
    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        if selectedRowIndex == indexPath.section {
            return 150  // I want the full cell size to be returned here (cell expanded)
        } else {
            return 75  // and here size returned only up to specific view (cell collapsed)
        }
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.beginUpdates()
        if selectedRowIndex == indexPath.section {
            selectedRowIndex = -1
        } else {
            selectedRowIndex = indexPath.section
        }
        tableView.endUpdates()
    }

    //another funcs...
}

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:

在此处输入图像描述

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:

在此处输入图像描述

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:

在此处输入图像描述

and, after tapping and scrolling a bit:

在此处输入图像描述

If it is a cell, put the content with constraints according to each other. For example, the top one to be 20 points from the top of the cell view. The middle one to be 30 points from the top element you already configured. That way, it doesn't matter how much content you put. Also I Didn't get it, what you want to be clicked, is it a button? Or if it is not, use a gesture recognizer.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM