英文:
UITableView inside UICollectionViewCell with the UITableView still scrollable, but touches should got to the collection view
问题
看起来是什么样子
这是一个单个CollectionView单元格的外观。
+-------------------------------+
| +-------+ |
| | 图片 | 一些文本 |
| +-------+ |
| |
| +---------------------------+ |
| + 表格视图行 1 | |
| +---------------------------+ |
| + 表格视图行 2 | |
| +---------------------------+ |
| + 表格视图行 3 | |
| +---------------------------+ |
+-------------------------------+
我想要实现的目标
根据显示的数据,表格中的条目可能是可选择的,也可能不可选择。但总是可能出现超过三行的情况,因此必须能够在垂直方向上滚动表格。但现在我也希望对于那些单行不可选择的表格,点击表格行将被传递给CollectionView,以便它能够识别触摸并触发适当的委托方法。
我到目前为止尝试过的方法
设置 tableView.allowsSelection := false
会导致表格中的行不可触摸。然而,点击似乎不会传递给CollectionView。
设置 tableView.isUserInteractionEnabled = false
会导致点击传递给CollectionView,但表格视图不再可滚动。
在 func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath?
中返回 nil
会导致表格中的行不可触摸。然而,点击似乎不会传递给CollectionView。
重写 func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
并在不应选择行的情况下返回 nil
会导致点击传递给CollectionView,但表格视图不再可滚动。
是否有任何方法可以实现这一点?
英文:
What it looks like
This is what a single CollectionView cell looks like.
+-------------------------------+
| +-------+ |
| | Image | Some text |
| +-------+ |
| |
| +---------------------------+ |
| + Table View row 1 | |
| +---------------------------+ |
| + Table View row 2 | |
| +---------------------------+ |
| + Table View row 3 | |
| +---------------------------+ |
+-------------------------------+
What I would like to achieve
Depending on the data displayed, the entries in the table may or may not be selectable.
But it can always happen that there are more than three rows, so you have to be able to scroll the table vertically.
But now I would also like that for the tables where the individual rows are not selectable, a touch on a table row is passed to the CollectionView so that it recognizes the touch and triggers the appropriate delegate method.
What I've tried so far
Setting tableView.allowsSelection := false
causes the rows in a table to be not touchable. However, the tap does not seem to be forwarded to the collection view.
Setting tableView.isUserInteractionEnabled = false
causes tap to be forwarded to the collection view, but the table view is no longer scrollable.
Returning nil
in func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath?
causes the rows in a table to be not touchable. However, the tap does not seem to be forwarded to the collection view.
Overriding func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
and return nil
when the rows should not be selectable, causes tap to be forwarded to the collection view, but the table view is no longer scrollable.
Is there any way to achieve this at all?
答案1
得分: 2
// 离开所有表视图单元格“可选择”
// 设置 `.selectionStyle = .none` 并实现自己的视觉变化以指示选定
// 在你的集合视图类中添加一个闭包到集合视图单元格
// 在集合视图单元格类中,在选择表行时调用该闭包
这是一个完整的示例...
**简单的单标签表视图单元格类** - 仅在 `canSelect` 为 `true` 时更改 contentView 的背景颜色:
```swift
class SomeTableCell: UITableViewCell {
static let identifier: String = "SomeTableCell"
var canSelect: Bool = true
let label = UILabel()
// ... (略)
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
if canSelect {
contentView.backgroundColor = selected ? .orange : .clear
}
}
}
集合视图单元格类 - 带有图像视图、标签和表视图... 重写 isSelected
以便我们可以看到单元格被选中... gotTap
闭包通知集合视图控制器表行已被选中:
class SomeCollectionCell: UICollectionViewCell, UITableViewDataSource, UITableViewDelegate {
// 闭包以便我们可以告诉控制器表行已被点击
// 无论是否可选择
var gotTap: ((UICollectionViewCell) -> ())?
static let identifier: String = "SomeCollectionCell"
let imgView = UIImageView()
let label = UILabel()
let tableView = UITableView()
var numTableRows: Int = 0 { didSet { tableView.reloadData() } }
var canSelectRows: Bool = true { didSet { tableView.reloadData() } }
// ... (略)
override var isSelected: Bool {
didSet {
contentView.backgroundColor = isSelected ? .systemBlue : .systemBackground
}
}
}
视图控制器类 - 我们将创建 30 个“数据”元素,每个集合视图单元格的表视图行数都会不同。我们会在每个第三个项目上“禁用”表行选择:
class SomeViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
// ... (略)
override func viewDidLoad() {
super.viewDidLoad()
// ... (略)
// 生成数据...
// 30个项目,每个随机 3 到 12 个表视图行
for _ in 0..<30 {
myData.append(Int.random(in: 3...12))
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return myData.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let c = collectionView.dequeueReusableCell(withReuseIdentifier: SomeCollectionCell.identifier, for: indexPath) as! SomeCollectionCell
c.imgView.image = UIImage(systemName: "\(indexPath.item).circle.fill")
c.label.text = "Item: \(indexPath.item)"
c.numTableRows = myData[indexPath.item]
// 每个第三个项目将具有不可选择的表视图行
c.canSelectRows = indexPath.row % 3 != 0
// 闭包以便集合视图单元格可以告诉我们表被点击了
c.gotTap = { [weak self] c in
guard let self = self,
let indexPath = self.collectionView.indexPath(for: c)
else { return }
// 告诉集合视图选择该项目
self.collectionView.selectItem(at: indexPath, animated: true, scrollPosition: [])
// 调用自定义函数“做某事”
self.myCollectionView(self.collectionView, didSelectItemAt: indexPath)
}
return c
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// 调用自定义函数“做某事”
myCollectionView(collectionView, didSelectItemAt: indexPath)
}
private func myCollectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Selected item: \(indexPath.item)")
}
}
英文:
One approach...
- leave all the table view cells "selectable"
- set
.selectionStyle = .none
and implement your own visual change to indicate selected - in your collection view class add a
closure
to the collection view cell - in collection view cell class, call that closure when a table row is selected
Here is a complete example...
simple, single-label table view cell class - change contentView background color only if canSelect
is true
:
class SomeTableCell: UITableViewCell {
static let identifier: String = "SomeTableCell"
var canSelect: Bool = true
let label = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
label.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(label)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: g.topAnchor),
label.leadingAnchor.constraint(equalTo: g.leadingAnchor),
label.trailingAnchor.constraint(equalTo: g.trailingAnchor),
label.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
//label.font = .systemFont(ofSize: 12.0, weight: .light)
label.font = .italicSystemFont(ofSize: 12.0)
// so we can see the framing
label.backgroundColor = .yellow
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
if canSelect {
contentView.backgroundColor = selected ? .orange : .clear
}
}
}
collection view cell class - with image view, label and table view... override isSelected
so we can see that the cell is selected... gotTap
closure to inform the collection view controller that a table row was selected:
class SomeCollectionCell: UICollectionViewCell, UITableViewDataSource, UITableViewDelegate {
// closure so we can tell the controller that a table row was tapped
// whether selectable or not
var gotTap: ((UICollectionViewCell) -> ())?
static let identifier: String = "SomeCollectionCell"
let imgView = UIImageView()
let label = UILabel()
let tableView = UITableView()
var numTableRows: Int = 0 { didSet { tableView.reloadData() } }
var canSelectRows: Bool = true { didSet { tableView.reloadData() } }
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
[imgView, label, tableView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(v)
}
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
imgView.topAnchor.constraint(equalTo: g.topAnchor),
imgView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
imgView.widthAnchor.constraint(equalToConstant: 60.0),
imgView.heightAnchor.constraint(equalToConstant: 40.0),
label.leadingAnchor.constraint(equalTo: imgView.trailingAnchor, constant: 8.0),
label.trailingAnchor.constraint(equalTo: g.trailingAnchor),
label.centerYAnchor.constraint(equalTo: imgView.centerYAnchor),
tableView.topAnchor.constraint(equalTo: imgView.bottomAnchor, constant: 8.0),
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
tableView.register(SomeTableCell.self, forCellReuseIdentifier: SomeTableCell.identifier)
tableView.dataSource = self
tableView.delegate = self
// element properties
imgView.contentMode = .scaleAspectFit
label.font = .systemFont(ofSize: 13.0, weight: .light)
contentView.layer.borderWidth = 1
contentView.layer.borderColor = UIColor.darkGray.cgColor
// so we can see the framing
imgView.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
label.backgroundColor = .cyan
tableView.backgroundColor = .green
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return numTableRows
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: SomeTableCell.identifier, for: indexPath) as! SomeTableCell
if canSelectRows {
c.label.text = "Sel row: \(indexPath.row)"
c.canSelect = true
} else {
c.label.text = "NON-Sel row: \(indexPath.row)"
c.canSelect = false
}
c.selectionStyle = .none
return c
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// either Always call the closure
gotTap?(self)
// or, only call it if the table rows are "not selectable"
//if !canSelectRows {
// gotTap?(self)
//}
}
override var isSelected: Bool {
didSet {
contentView.backgroundColor = isSelected ? .systemBlue : .systemBackground
}
}
}
view controller class - we'll create 30 "data" elements, with the number of table view rows for each collection view cell. We will "disable" table row selection on every 3rd item:
class SomeViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
var collectionView: UICollectionView!
var myData: [Int] = []
override func viewDidLoad() {
super.viewDidLoad()
let fl = UICollectionViewFlowLayout()
fl.scrollDirection = .horizontal
fl.itemSize = .init(width: 160.0, height: 164.0)
collectionView = UICollectionView(frame: .zero, collectionViewLayout: fl)
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
collectionView.register(SomeCollectionCell.self, forCellWithReuseIdentifier: SomeCollectionCell.identifier)
collectionView.dataSource = self
collectionView.delegate = self
// let's generate data...
// 30 items, each with random 3-to-12 table rows
for _ in 0..<30 {
myData.append(Int.random(in: 3...12))
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return myData.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let c = collectionView.dequeueReusableCell(withReuseIdentifier: SomeCollectionCell.identifier, for: indexPath) as! SomeCollectionCell
c.imgView.image = UIImage(systemName: "\(indexPath.item).circle.fill")
c.label.text = "Item: \(indexPath.item)"
c.numTableRows = myData[indexPath.item]
// every 3rd item will have non-selectable table rows
c.canSelectRows = indexPath.row % 3 != 0
// closure so the collection view cell can tell us the table was tapped
c.gotTap = { [weak self] c in
guard let self = self,
let indexPath = self.collectionView.indexPath(for: c)
else { return }
// tell the collection view to select that item
self.collectionView.selectItem(at: indexPath, animated: true, scrollPosition: [])
// call custom func to "do something"
self.myCollectionView(self.collectionView, didSelectItemAt: indexPath)
}
return c
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// call custom func to "do something"
myCollectionView(collectionView, didSelectItemAt: indexPath)
}
private func myCollectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Selected item: \(indexPath.item)")
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论