ios - 适用于 iOS 11+ 的可扩展自定义 UITableViewCell
问题描述
我发现很多人都以各种形式提出过这个问题,答案也铺天盖地,所以我总结一下我的具体情况,希望能得到更具体的答案。首先,我正在为 iOS 11+ 构建并拥有相对较新的 XCode 版本(11+)。也许不是最新的,但足够近了。
基本上,我需要一个自定大小的表格视图,当用户与它们交互时,单元格可能会在运行时展开和折叠。在viewDidLoad
我将rowHeightUITableView.automaticDimension
和estimatedRowHeight 设置为大于44 的固定值的某个数字。但单元格并没有像它应该的那样扩展,即使我似乎已经尝试了书中的每一个建议。
如果这很重要,我有一个表格单元格的自定义类,但没有 .XIB 文件 - UI 直接在原型中定义。我尝试了许多其他变体,但感觉最简单的方法是让 UIStackView 成为原型的唯一直接子代(可以说“收入”功能都在其中。在我的情况下,它们包括一个标签和另一个tableview - 我嵌套3层深 - 但这可能不是重点)并将它的所有4个边缘约束到父级。我已经尝试过了,我已经修改了堆栈视图中的分布(填充、均匀填充、按比例填充),但似乎都不起作用。我该怎么做才能使细胞正常膨胀?
万一有人想知道,我曾经覆盖heightForRowAt
,但现在我没有,因为在运行时预测高度并不容易,我希望这个过程可以自动化。
解决方案
从基础开始...
UIStackView
这是一个带有两个标签的垂直:
红色轮廓显示堆栈视图的框架。
如果我们点击按钮,它将设置bottomLabel.isHidden = true
:
请注意,除了被隐藏之外,堆栈视图还会删除它所占用的空间。
现在,我们可以在表格视图单元格中使用堆栈视图来实现展开/折叠功能。
我们将从扩展的每隔一行开始:
现在我们点击第 1 行的“折叠”按钮,我们得到:
不完全是我们想要的。我们成功地“折叠”了单元格内容,但表格视图对此一无所知。
所以,我们可以添加一个闭包......当我们点击按钮时,单元格中的代码将显示/隐藏底部标签,并且它将使用闭包告诉表格视图发生了什么。我们的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
}
我们得到:
这是一个完整的例子:
简单的“虚线轮廓视图”
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)", 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
}
}
推荐阅读
- java - 运行带有来自 java 的用户交互的 shell 脚本
- xpath - 如何识别出现在 XML 文档中不同级别的元素的不同 xpath?
- botframework - 如何在 Bot Composer 的正则表达式中使用捕获组
- sql - SQL如何计算不基于行的中位数
- python - 安装pybind11时cmake出现问题
- python - 绘制堆栈中的能级,相互叠加
- flutter - 如何使 TabBar 在 SliverAppBar 中不动?
- html - 在 JSF textArea 中保留标签 (\t)
- java - 如何通过添加周来更改 LocalDate 的年份?
- .net - 如何使用“.Net FrameWork 4.7”项目在 Azure DevOps Pipeline 上创建 Coverlet 覆盖率报告?