以编程方式创建可扩展的UITableView单元格。

3
我有一个想要在点击时展开和折叠的tableviewcell. 所有我找到的示例都是基于storyboard的,而我正在尝试以编程方式完成。最初我的想法是创建一个子视图并将其约束到内容视图,但是当我使用heightForRowAt调整单元格的高度时,它也增加了内容视图的大小(这是有道理的)。你觉得我该怎么做呢?
这里是我想要发生的可视化参考enter image description here
3个回答

7
使用垂直UIStackView,并将底部视图isHidden设置为true,然后在点击(或任何触发展开的事件)时,只需将isHidden = false。考虑到UIStackView如何处理isHidden,我想这应该是最简单的方法之一。另一种方法是设置自动布局约束,并通过将NSLayoutConstraint的常量设置为0来更改底部视图的高度锚点。

无论选择哪种方法,您都必须告诉tableView刷新其显示(从viewcontroller):

func refreshTableAfterCellExpansion() {
    self.tableView.beginUpdates()
    self.tableView.setNeedsDisplay()
    self.tableView.endUpdates()
}

例如,查看以下SO问题及其答案
使用playgrounds的示例(一个使用UIStackView,另一个使用相同原理):
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()

啊,好的,我以前从没用过UIStackView,我得去看一下。 - sanch
@MattSanford 我已经添加了一个游乐场示例,你可以用作起点。 - Milan Nosáľ
当元素从隐藏状态变为非隐藏状态时,我在内容视图上不断遇到高度限制问题。 - sanch
这是解决方案,它可以正常工作,我只想摆脱控制台中的错误。 - sanch
有什么错误?不可满足的约束条件吗?如果是这种情况,并且其中一个是 UIView-Encapsulated-Layout-HeightUIView-Encapsulated-Layout-Width,请查看我对此问题的回答(这是动态高度单元格的已知问题):https://stackoverflow.com/a/47937781/2912282 - Milan Nosáľ

0
我会简单地定义两种单元格类型,并在tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)中选择适当的一种。

你是说我要删除“collapsed”版本并插入展开的行吗?我读到过与此相关的性能问题。 - sanch
实现该想法的方法是在tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)中调用对应于折叠或展开给定行的标识符的dequeueReusableCell。我还没有考虑性能方面。 - claude31

-1

您可以使用标志来指示特定的部分或行是否已展开,并在UITableView上使用以下方法:

    reloadRows()
    reloadSections()

将heightForRow方法更改为根据标志返回所需的高度。 如果您想要同时展开多行,则可以为数据源中的每个项目使用ViewModel,并在调用didSelectItemAt或didDeselectItemAt方法时更新其中的标志,然后使用所需的动画重新加载行或部分。

我尝试过这个,但问题在于我不确定在哪里附加子视图以便正确显示。 - sanch
如果您使用reloadRows或reloadSections,这将调用cellForRow方法,您可以在那里调整视图,您可能希望始终添加视图,但使用0高度约束,然后在cellForRow方法中更改视图的高度约束。 - MohammadN

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接