sanch
sanch

Reputation: 716

Programmatically creating an expanding UItableViewCell

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?

For visual reference here is what I want to happenenter image description here

Upvotes: 3

Views: 3035

Answers (3)

Milan Nosáľ
Milan Nosáľ

Reputation: 19737

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()

Upvotes: 7

MohammadN
MohammadN

Reputation: 1

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.

Upvotes: -1

claude31
claude31

Reputation: 884

I would simply define 2 cell types, and select the one which is appropriate in tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)

Upvotes: 0

Related Questions