简体   繁体   English

iOS 11+ 的可扩展自定义 UITableViewCell

[英]Expandable custom UITableViewCell for iOS 11+

I realize that many people have asked this question in various forms and the answers are all over the page, so let me summarize my specific situation in hopes of getting more specific answers.我知道很多人在各种forms中都问过这个问题,答案遍布整个页面,所以让我总结一下我的具体情况,希望得到更具体的答案。 First of all, I'm building for iOS 11+ and have a relatively recent version of XCode (11+).首先,我正在为 iOS 11+ 构建,并且拥有相对较新的 XCode (11+) 版本。 Maybe not the latest, but recent enough.也许不是最新的,但足够近了。

Basically, I need a self-sizing tableview where the cells may expand and collapse at runtime when the user interacts with them.基本上,我需要一个自定大小的表格视图,当用户与它们交互时,单元格可能会在运行时展开和折叠。 In viewDidLoad I set the rowHeight to UITableView.automaticDimension and estimatedRowHeight to some number that's bigger than the canned value of 44. But the cell is not expanding like it should, even though I seem to have tried every bit of advice in the book.viewDidLoad中,我将 rowHeight 设置为UITableView.automaticDimension并将estimatedRowHeight 设置为大于预设值 44 的某个数字。但单元格并没有像应有的那样扩展,即使我似乎已经尝试了书中的每一个建议。

If that matters, I have a custom class for the table cell but no.XIB file for it - the UI is defined directly in the prototype.如果这很重要,我有一个用于表格单元格的自定义 class 但没有用于它的.XIB 文件 - UI 直接在原型中定义。 I've tried a number of other variations, but it feels like the easiest is making a UIStackView the only direct child of the prototype (the "revenue" features so to speak would all be inside it. In my case, they include a label and another tableview - I nest 3 levels deep - but that's probably beside the point) and constraining all 4 of it's edges to the parent.我尝试了许多其他变体,但感觉最简单的方法是让 UIStackView 成为原型的唯一直接子项(可以说“收入”功能都在其中。就我而言,它们包括 label和另一个tableview - 我嵌套3层深 - 但这可能不是重点)并将它的所有4个边缘约束到父级。 I've tried that, and I've tinkered with the distribution in the stack view (Fill, Fill Evenly, Fill Proportionately), but none of it seems to work.我已经尝试过了,我已经修改了堆栈视图中的分布(填充、均匀填充、按比例填充),但似乎都不起作用。 What can I do to make the cells expand properly?我该怎么做才能使细胞正常膨胀?

In case anyone's wondering, I used to override heightForRowAt but now I don't because it's not easy to predict the height at runtime and I'm hoping the process could be automated.万一有人想知道,我曾经覆盖heightForRowAt但现在我没有,因为在运行时预测高度并不容易,我希望这个过程可以自动化。

Start with the basics...从基础开始...

Here is a vertical UIStackView with two labels:这是一个带有两个标签的垂直UIStackView

在此处输入图像描述

The red outline shows the frame of the stack view.红色轮廓显示堆栈视图的框架。

If we tap the button, it will set bottomLabel.isHidden = true :如果我们点击按钮,它将设置bottomLabel.isHidden = true

在此处输入图像描述

Notice that in addition to being hidden, the stack view removes the space it was occupying.请注意,除了被隐藏之外,堆栈视图还会删除它所占用的空间。

Now, we can do that with a stack view in a table view cell to get expand/collapse functionality.现在,我们可以在表格视图单元格中使用堆栈视图来实现展开/折叠功能。

We'll start with every-other row expanded:我们将从扩展的每隔一行开始:

在此处输入图像描述

Now we tap the "Collapse" button for row 1 and we get:现在我们点击第 1 行的“折叠”按钮,我们得到:

在此处输入图像描述

Not quite what we want.不完全是我们想要的。 We successfully "collapsed" the cell content, but the table view doesn't know anything about it.我们成功地“折叠”了单元格内容,但表格视图对此一无所知。

So, we can add a closure... when we tap the button, the code in the cell will show/hide the bottom label AND it will use the closure to tell the table view what happened.所以,我们可以添加一个闭包......当我们点击按钮时,单元格中的代码将显示/隐藏底部 label并且它将使用闭包告诉表格视图发生了什么。 Our cellForRowAt func looks like this:我们的cellForRowAt函数如下所示:

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
}

and we get:我们得到:

在此处输入图像描述

Here's a complete example:这是一个完整的例子:

Simple "dashed outline view"简单的“虚线轮廓视图”

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
    }
}

The cell class电池 class

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

and a table view controller to demonstrate和一个表格视图 controller 来演示

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)", 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
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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