适用于 iOS 11+

问题描述

我发现很多人都以各种形式问过这个问题,而且答案也很多,所以我总结一下我的具体情况,希望能得到更具体的答案。首先,我正在为 iOS 11+ 构建并且拥有相对较新版本的 XCode(11+)。也许不是最新的,但足够新。

基本上,我需要一个自动调整大小的表格视图,当用户与它们交互时,其中的单元格可以在运行时展开和折叠。在 viewDidLoad 中,我将 rowHeight 设置为 UITableView.automaticDimension,并将estimatedRowHeight 设置为大于固定值 44 的某个数字。但是单元格并没有像它应该的那样扩展,尽管我似乎已经尝试了书中的建议。

如果这很重要,我有一个用于表格单元格的自定义类,但没有 .XIB 文件 - UI 直接在原型中定义。我已经尝试了许多其他变体,但感觉最简单的是让 UIStackView 成为原型的唯一直接子元素(可以说“收入”功能都在其中。就我而言,它们包括一个标签和另一个 tableview - 我嵌套 3 层深 - 但这可能不是重点)并将它的所有 4 个边缘限制到父级。我已经尝试过,并且我已经修改了堆栈视图中的分布(填充、均匀填充、按比例填充),但似乎都不起作用。我该怎么做才能使细胞正确扩增?

如果有人想知道,我曾经覆盖 heightForRowAt,但现在我没有,因为在运行时预测高度并不容易,我希望这个过程可以自动化。

解决方法

从基础开始...

这是一个带有两个标签的垂直 UIStackView

enter image description here

红色轮廓显示堆栈视图的框架。

如果我们点击按钮,它会设置bottomLabel.isHidden = true

enter image description here

请注意,除了被隐藏之外,堆栈视图还会移除它所占用的空间。

现在,我们可以使用表格视图单元格中的堆栈视图来实现展开/折叠功能。

我们将从每隔一行展开:

enter image description here

现在我们点击第 1 行的“折叠”按钮,我们得到:

enter image description here

不是我们想要的。我们成功地“折叠”了单元格内容,但表格视图对此一无所知。

所以,我们可以添加一个闭包...当我们点击按钮时,单元格中的代码将显示/隐藏底部标签AND它将使用关闭告诉表视图发生了什么。我们的 cellForRowAt func 看起来像这样:

override func tableView(_ tableView: UITableView,cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let c = tableView.dequeueReusableCell(withIdentifier: "c",for: indexPath) as! ExpColCell

    c.setData("Top \(indexPath.row)",str2: "Bottom \(indexPath.row)\n2\n3\n4\n5",isCollapsed: isCollapsedArray[indexPath.row])

    c.didChangeHeight = { [weak self] isCollapsed in
        guard let self = self else { return }
        // update our data source
        self.isCollapsedArray[indexPath.row] = isCollapsed
        // tell the tableView to re-run its layout
        self.tableView.performBatchUpdates(nil,completion: nil)
    }

    return c
}

我们得到:

enter image description here

这是一个完整的例子:

简单的“虚线轮廓视图”

class DashedOutlineView: UIView {
    
    @IBInspectable var dashColor: UIColor = .red
    var shapeLayer: CAShapeLayer!
    
    override class var layerClass: AnyClass {
        return CAShapeLayer.self
    }
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        shapeLayer = self.layer as? CAShapeLayer
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.lineWidth = 1.0
        shapeLayer.lineDashPattern = [8,8]
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        shapeLayer.strokeColor = dashColor.cgColor
        shapeLayer.path = UIBezierPath(rect: bounds).cgPath
    }
}

细胞类

class ExpColCell: UITableViewCell {

    public var didChangeHeight: ((Bool) -> ())?
    
    private let stack = UIStackView()
    private let topLabel = UILabel()
    private let botLabel = UILabel()
    private let toggleButton = UIButton()
    
    private let outlineView = DashedOutlineView()
    
    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 {
        // button properties
        toggleButton.translatesAutoresizingMaskIntoConstraints = false
        toggleButton.backgroundColor = .systemBlue
        toggleButton.setTitleColor(.white,for: .normal)
        toggleButton.setTitleColor(.gray,for: .highlighted)
        toggleButton.setTitle("Collapse",for: [])
        
        // label properties
        topLabel.text = "Top Label"
        botLabel.text = "Bottom Label"
        topLabel.font = .systemFont(ofSize: 32.0)
        botLabel.font = .italicSystemFont(ofSize: 24.0)
        topLabel.backgroundColor = .green
        botLabel.backgroundColor = .systemTeal
        
        botLabel.numberOfLines = 0
        
        // outline view properties
        outlineView.translatesAutoresizingMaskIntoConstraints = false
        
        // stack view properties
        stack.translatesAutoresizingMaskIntoConstraints = false
        stack.axis = .vertical
        stack.spacing = 8
        
        // add the labels
        stack.addArrangedSubview(topLabel)
        stack.addArrangedSubview(botLabel)
        
        // add outlineView,stack view and button to contentView
        contentView.addSubview(outlineView)
        contentView.addSubview(stack)
        contentView.addSubview(toggleButton)
        
        // we'll use the margin guide
        let g = contentView.layoutMarginsGuide
        
        NSLayoutConstraint.activate([
            
            stack.topAnchor.constraint(equalTo: g.topAnchor),stack.leadingAnchor.constraint(equalTo: g.leadingAnchor),outlineView.topAnchor.constraint(equalTo: stack.topAnchor),outlineView.leadingAnchor.constraint(equalTo: stack.leadingAnchor),outlineView.trailingAnchor.constraint(equalTo: stack.trailingAnchor),outlineView.bottomAnchor.constraint(equalTo: stack.bottomAnchor),toggleButton.topAnchor.constraint(equalTo: g.topAnchor),toggleButton.trailingAnchor.constraint(equalTo: g.trailingAnchor),toggleButton.leadingAnchor.constraint(equalTo: stack.trailingAnchor,constant: 16.0),toggleButton.widthAnchor.constraint(equalToConstant: 92.0),])
        
        // we set the bottomAnchor constraint like this to avoid intermediary auto-layout warnings
        let c = stack.bottomAnchor.constraint(equalTo: g.bottomAnchor)
        c.priority = UILayoutPriority(rawValue: 999)
        c.isActive = true

        // set label Hugging and Compression to prevent them from squeezing/stretching
        topLabel.setContentHuggingPriority(.required,for: .vertical)
        topLabel.setContentCompressionResistancePriority(.required,for: .vertical)
        botLabel.setContentHuggingPriority(.required,for: .vertical)
        botLabel.setContentCompressionResistancePriority(.required,for: .vertical)

        contentView.clipsToBounds = true
        
        toggleButton.addTarget(self,action: #selector(toggleButtonTapped),for: .touchUpInside)
        
    }
    
    func setData(_ str1: String,str2: String,isCollapsed: Bool) -> Void {
        topLabel.text = str1
        botLabel.text = str2
        botLabel.isHidden = isCollapsed
        updateButtonTitle()
    }
    func updateButtonTitle() -> Void {
        let t = botLabel.isHidden ? "Expand" : "Collapse"
        toggleButton.setTitle(t,for: [])
    }
    
    @objc func toggleButtonTapped() -> Void {
        botLabel.isHidden.toggle()
        updateButtonTitle()
        
        // comment / un-comment this line to see the difference
        didChangeHeight?(botLabel.isHidden)
    }
}

和一个表视图控制器来演示

class ExpColTableViewController: UITableViewController {

    var isCollapsedArray: [Bool] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.register(ExpColCell.self,forCellReuseIdentifier: "c")
        
        // 16 "rows" start with every-other row collapsed
        for i in 0..<15 {
            isCollapsedArray.append(i % 2 == 0)
        }
        
    }
    
    override func tableView(_ tableView: UITableView,numberOfRowsInSection section: Int) -> Int {
        return isCollapsedArray.count
    }
    override func tableView(_ tableView: UITableView,cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let c = tableView.dequeueReusableCell(withIdentifier: "c",for: indexPath) as! ExpColCell

        c.setData("Top \(indexPath.row)",isCollapsed: isCollapsedArray[indexPath.row])

        c.didChangeHeight = { [weak self] isCollapsed in
            guard let self = self else { return }
            // update our data source
            self.isCollapsedArray[indexPath.row] = isCollapsed
            // tell the tableView to re-run its layout
            self.tableView.performBatchUpdates(nil,completion: nil)
        }

        return c
    }
}