扩展 UITableViewCell 添加 UICollectionView 作为子视图
Expand UITableViewCell adding UICollectionView as a subview
我已经查看了其他几个 Stack Overflow 问题,但一直找不到能回答我所遇到的特定问题的问题。很多答案都说要使 table 视图部分可扩展,但我希望该行是可扩展的,并且能够在扩展时向该行添加一个集合视图。而且,最好不要任何外部库。
我有一个 UITableView 部分,其中包含可以点击展开或折叠的行。折叠时,单元格在左侧的垂直 UIStackView 中显示两行文本,在右侧显示图像和向下箭头。下图显示了折叠视图,我可以使用 AutoLayout 约束使其正常工作。
展开时,应在单元格内添加一个 UICollectionView,并且该行应动画展开以显示集合视图。下图大致显示了它应该是什么样子。底部的灰色区域是collection view,它是水平动态collection view。
但我不确定如何将集合视图添加到 UITableViewCell 以及如何在其中设置动画。我考虑过在代码中创建集合视图,向左右添加约束以将其固定到行的边缘(因此它将是全宽)。然后我打算在集合视图的顶部添加一个约束以将其固定到 table 视图单元格的底部,然后在动画块中,我将更改约束以便collection view 在 table view cell 的底部,collection view 的顶部被约束在 image view 的底部,设置 compression resistance priority 为 1000 以确保 collection view 不被压缩,但是我最终遇到了无法满足的约束,我的约束最终被删除了。我最终放弃了那个想法,因为我觉得它太复杂了,应该有更直接的方法来做这件事。
我考虑过的另一种方法是在同一个 XIB 文件中有两个单独的视图 - 一个用于正常状态,一个用于展开状态。但是后来我不确定如何为状态之间的行中的所有变化设置动画。
有没有人知道如何在 table 视图单元展开时将集合视图(或我猜的任何视图)添加到底部以及如何为其设置动画?做这样的事情的最佳做法是什么?感谢您的帮助。
我会建议...
- 将子视图作为集合视图“容器”添加到您的单元格
- 设置其
.clipsToBounds = true
- 用
.constant = 0
给它一个高度限制
- 使用
.constant = 0
给 collectionView 一个高度限制
- 当您知道所需的 collectionViewCell 高度时,设置 collectionView 的约束
.constant
以便它加载单元格
- 将“容器”视图的高度限制动画化为 expand/collapse
从“容器”视图本身开始...一旦您正确展开/折叠,然后添加集合视图。
编辑 这是动画 expand/collapse table 视图单元格的一个非常基本的示例。
data struct - 键和值字符串,一个“高度”属性(当您知道集合视图的高度时,大概会设置它行)和一个“扩展”标志。
struct DemoStruct {
var key: String = ""
var value: String = ""
var cvHeight: CGFloat = 0
var expanded: Bool = false
}
cell class - 两个标签,expand/collapse 按钮,标签下方的“容器”视图:
class SampleTVCell: UITableViewCell {
// always visible elements
let keyLabel = UILabel()
let valLabel = UILabel()
let btn = UIButton()
// this will either hold the collectionView
// or be replaced by the collectionView
let containerView = UIView()
// will be set by the data
var containerHeight: NSLayoutConstraint!
// constraints for expanded/collapsed states
var expandedConstraint: NSLayoutConstraint!
var collapsedConstraint: NSLayoutConstraint!
// down/up chevrons
var expandImg: UIImage!
var collapseImg: UIImage!
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 {
// make sure we get down/up button images
guard let imgD = UIImage(systemName: "chevron.down"),
let imgU = UIImage(systemName: "chevron.up")
else {
fatalError("Bad image loading!")
}
expandImg = imgD
collapseImg = imgU
// covers the bottom of the container view when cell is "collapsed"
let coverView = UIView()
coverView.backgroundColor = .white
[keyLabel, valLabel, btn, containerView, coverView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(v)
}
// set as desired
keyLabel.font = .systemFont(ofSize: 20.0, weight: .bold)
valLabel.font = .italicSystemFont(ofSize: 18.0)
// use the built-in layout margins
let g = contentView.layoutMarginsGuide
// .constant will be set by data
containerHeight = containerView.heightAnchor.constraint(equalToConstant: 0.0)
// constrain bottom of valLabel to bottom of margins
// priority will be updated when expanded/collapsed
collapsedConstraint = valLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor)
collapsedConstraint.priority = .defaultHigh
// constrain bottom of containerView to bottom of margins
// priority will be updated when expanded/collapsed
expandedConstraint = containerView.bottomAnchor.constraint(equalTo: g.bottomAnchor)
expandedConstraint.priority = .defaultLow
NSLayoutConstraint.activate([
// button at upper-right
btn.topAnchor.constraint(equalTo: g.topAnchor),
btn.trailingAnchor.constraint(equalTo: g.trailingAnchor),
btn.heightAnchor.constraint(equalToConstant: 32.0),
btn.widthAnchor.constraint(equalTo: btn.heightAnchor, multiplier: 1.5),
// keyLabel at upper-left
keyLabel.topAnchor.constraint(equalTo: g.topAnchor),
keyLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
keyLabel.trailingAnchor.constraint(equalTo: btn.leadingAnchor, constant: -8.0),
// valLabel aligned and below keyLabel
valLabel.topAnchor.constraint(equalTo: keyLabel.bottomAnchor, constant: 4.0),
valLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
valLabel.trailingAnchor.constraint(equalTo: btn.leadingAnchor, constant: -8.0),
// containerView below valLabel
containerView.topAnchor.constraint(equalTo: valLabel.bottomAnchor, constant: 8.0),
containerView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
// activate our "control" constraints
collapsedConstraint,
expandedConstraint,
containerHeight,
// cover view sits at the very bottom of the contentView
// to cover the top part of the containerView
// when cell is "collapsed"
coverView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
coverView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
coverView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
coverView.heightAnchor.constraint(equalToConstant: contentView.layoutMargins.bottom),
])
// we don't want the labels to stretch or compress vertically
keyLabel.setContentCompressionResistancePriority(.required, for: .vertical)
valLabel.setContentCompressionResistancePriority(.required, for: .vertical)
keyLabel.setContentHuggingPriority(.required, for: .vertical)
valLabel.setContentHuggingPriority(.required, for: .vertical)
// action for button
btn.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
// Important!
contentView.clipsToBounds = true
// so we can see the containerView
// until we replace it with or add a collectionView as a subview
containerView.backgroundColor = .systemYellow
}
// closure so we can tell the controller we want the cell
// to collapse or expand
var expand: ((Bool)->())?
private var expanded: Bool = false {
didSet {
// set collapsed/expanded constraint priorities
collapsedConstraint.priority = expanded ? .defaultLow : .defaultHigh
expandedConstraint.priority = expanded ? .defaultHigh : .defaultLow
// update button image
btn.setImage(expanded ? collapseImg : expandImg, for: [])
}
}
func fillData(_ d: DemoStruct) -> Void {
keyLabel.text = d.key
valLabel.text = d.value
containerHeight.constant = d.cvHeight
expanded = d.expanded
}
@objc func btnTap(_ sender: Any?) -> Void {
expanded.toggle()
// tell the controller our expanded state changed
expand?(expanded)
}
}
控制器 class - 生成 20 行数据,具有不同的“集合视图”高度。
class DemoTableViewController: UITableViewController {
var theData: [DemoStruct] = []
override func viewDidLoad() {
super.viewDidLoad()
// create 20 rows of sample data
theData = (1...20).map { DemoStruct(key: "Key \([=12=])", value: "Value \([=12=])", cvHeight: 40.0, expanded: false) }
// vary the containerView heights
// this will end up being the collectionView heights
let demoHeights: [CGFloat] = [
40, 160, 80, 120,
]
for i in 0..<theData.count {
theData[i].cvHeight = demoHeights[i % demoHeights.count]
}
tableView.register(SampleTVCell.self, forCellReuseIdentifier: "stvc")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return theData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "stvc", for: indexPath) as! SampleTVCell
let d = theData[indexPath.row]
c.fillData(d)
// set the closure
c.expand = { isExpanded in
// update data
self.theData[indexPath.row].expanded = isExpanded
// this tells the tableView to animate cell height changes
tableView.performBatchUpdates(nil, completion: nil)
}
return c
}
}
我已经查看了其他几个 Stack Overflow 问题,但一直找不到能回答我所遇到的特定问题的问题。很多答案都说要使 table 视图部分可扩展,但我希望该行是可扩展的,并且能够在扩展时向该行添加一个集合视图。而且,最好不要任何外部库。
我有一个 UITableView 部分,其中包含可以点击展开或折叠的行。折叠时,单元格在左侧的垂直 UIStackView 中显示两行文本,在右侧显示图像和向下箭头。下图显示了折叠视图,我可以使用 AutoLayout 约束使其正常工作。
展开时,应在单元格内添加一个 UICollectionView,并且该行应动画展开以显示集合视图。下图大致显示了它应该是什么样子。底部的灰色区域是collection view,它是水平动态collection view。
但我不确定如何将集合视图添加到 UITableViewCell 以及如何在其中设置动画。我考虑过在代码中创建集合视图,向左右添加约束以将其固定到行的边缘(因此它将是全宽)。然后我打算在集合视图的顶部添加一个约束以将其固定到 table 视图单元格的底部,然后在动画块中,我将更改约束以便collection view 在 table view cell 的底部,collection view 的顶部被约束在 image view 的底部,设置 compression resistance priority 为 1000 以确保 collection view 不被压缩,但是我最终遇到了无法满足的约束,我的约束最终被删除了。我最终放弃了那个想法,因为我觉得它太复杂了,应该有更直接的方法来做这件事。
我考虑过的另一种方法是在同一个 XIB 文件中有两个单独的视图 - 一个用于正常状态,一个用于展开状态。但是后来我不确定如何为状态之间的行中的所有变化设置动画。
有没有人知道如何在 table 视图单元展开时将集合视图(或我猜的任何视图)添加到底部以及如何为其设置动画?做这样的事情的最佳做法是什么?感谢您的帮助。
我会建议...
- 将子视图作为集合视图“容器”添加到您的单元格
- 设置其
.clipsToBounds = true
- 用
.constant = 0
给它一个高度限制
- 使用
.constant = 0
给 collectionView 一个高度限制
- 当您知道所需的 collectionViewCell 高度时,设置 collectionView 的约束
.constant
以便它加载单元格 - 将“容器”视图的高度限制动画化为 expand/collapse
从“容器”视图本身开始...一旦您正确展开/折叠,然后添加集合视图。
编辑 这是动画 expand/collapse table 视图单元格的一个非常基本的示例。
data struct - 键和值字符串,一个“高度”属性(当您知道集合视图的高度时,大概会设置它行)和一个“扩展”标志。
struct DemoStruct {
var key: String = ""
var value: String = ""
var cvHeight: CGFloat = 0
var expanded: Bool = false
}
cell class - 两个标签,expand/collapse 按钮,标签下方的“容器”视图:
class SampleTVCell: UITableViewCell {
// always visible elements
let keyLabel = UILabel()
let valLabel = UILabel()
let btn = UIButton()
// this will either hold the collectionView
// or be replaced by the collectionView
let containerView = UIView()
// will be set by the data
var containerHeight: NSLayoutConstraint!
// constraints for expanded/collapsed states
var expandedConstraint: NSLayoutConstraint!
var collapsedConstraint: NSLayoutConstraint!
// down/up chevrons
var expandImg: UIImage!
var collapseImg: UIImage!
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 {
// make sure we get down/up button images
guard let imgD = UIImage(systemName: "chevron.down"),
let imgU = UIImage(systemName: "chevron.up")
else {
fatalError("Bad image loading!")
}
expandImg = imgD
collapseImg = imgU
// covers the bottom of the container view when cell is "collapsed"
let coverView = UIView()
coverView.backgroundColor = .white
[keyLabel, valLabel, btn, containerView, coverView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(v)
}
// set as desired
keyLabel.font = .systemFont(ofSize: 20.0, weight: .bold)
valLabel.font = .italicSystemFont(ofSize: 18.0)
// use the built-in layout margins
let g = contentView.layoutMarginsGuide
// .constant will be set by data
containerHeight = containerView.heightAnchor.constraint(equalToConstant: 0.0)
// constrain bottom of valLabel to bottom of margins
// priority will be updated when expanded/collapsed
collapsedConstraint = valLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor)
collapsedConstraint.priority = .defaultHigh
// constrain bottom of containerView to bottom of margins
// priority will be updated when expanded/collapsed
expandedConstraint = containerView.bottomAnchor.constraint(equalTo: g.bottomAnchor)
expandedConstraint.priority = .defaultLow
NSLayoutConstraint.activate([
// button at upper-right
btn.topAnchor.constraint(equalTo: g.topAnchor),
btn.trailingAnchor.constraint(equalTo: g.trailingAnchor),
btn.heightAnchor.constraint(equalToConstant: 32.0),
btn.widthAnchor.constraint(equalTo: btn.heightAnchor, multiplier: 1.5),
// keyLabel at upper-left
keyLabel.topAnchor.constraint(equalTo: g.topAnchor),
keyLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
keyLabel.trailingAnchor.constraint(equalTo: btn.leadingAnchor, constant: -8.0),
// valLabel aligned and below keyLabel
valLabel.topAnchor.constraint(equalTo: keyLabel.bottomAnchor, constant: 4.0),
valLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
valLabel.trailingAnchor.constraint(equalTo: btn.leadingAnchor, constant: -8.0),
// containerView below valLabel
containerView.topAnchor.constraint(equalTo: valLabel.bottomAnchor, constant: 8.0),
containerView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
// activate our "control" constraints
collapsedConstraint,
expandedConstraint,
containerHeight,
// cover view sits at the very bottom of the contentView
// to cover the top part of the containerView
// when cell is "collapsed"
coverView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
coverView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
coverView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
coverView.heightAnchor.constraint(equalToConstant: contentView.layoutMargins.bottom),
])
// we don't want the labels to stretch or compress vertically
keyLabel.setContentCompressionResistancePriority(.required, for: .vertical)
valLabel.setContentCompressionResistancePriority(.required, for: .vertical)
keyLabel.setContentHuggingPriority(.required, for: .vertical)
valLabel.setContentHuggingPriority(.required, for: .vertical)
// action for button
btn.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
// Important!
contentView.clipsToBounds = true
// so we can see the containerView
// until we replace it with or add a collectionView as a subview
containerView.backgroundColor = .systemYellow
}
// closure so we can tell the controller we want the cell
// to collapse or expand
var expand: ((Bool)->())?
private var expanded: Bool = false {
didSet {
// set collapsed/expanded constraint priorities
collapsedConstraint.priority = expanded ? .defaultLow : .defaultHigh
expandedConstraint.priority = expanded ? .defaultHigh : .defaultLow
// update button image
btn.setImage(expanded ? collapseImg : expandImg, for: [])
}
}
func fillData(_ d: DemoStruct) -> Void {
keyLabel.text = d.key
valLabel.text = d.value
containerHeight.constant = d.cvHeight
expanded = d.expanded
}
@objc func btnTap(_ sender: Any?) -> Void {
expanded.toggle()
// tell the controller our expanded state changed
expand?(expanded)
}
}
控制器 class - 生成 20 行数据,具有不同的“集合视图”高度。
class DemoTableViewController: UITableViewController {
var theData: [DemoStruct] = []
override func viewDidLoad() {
super.viewDidLoad()
// create 20 rows of sample data
theData = (1...20).map { DemoStruct(key: "Key \([=12=])", value: "Value \([=12=])", cvHeight: 40.0, expanded: false) }
// vary the containerView heights
// this will end up being the collectionView heights
let demoHeights: [CGFloat] = [
40, 160, 80, 120,
]
for i in 0..<theData.count {
theData[i].cvHeight = demoHeights[i % demoHeights.count]
}
tableView.register(SampleTVCell.self, forCellReuseIdentifier: "stvc")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return theData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "stvc", for: indexPath) as! SampleTVCell
let d = theData[indexPath.row]
c.fillData(d)
// set the closure
c.expand = { isExpanded in
// update data
self.theData[indexPath.row].expanded = isExpanded
// this tells the tableView to animate cell height changes
tableView.performBatchUpdates(nil, completion: nil)
}
return c
}
}