英文:
Coordinated Scrolling Between Two TableViews
问题
我有两个重叠的UITableView
,只能垂直滚动。我想要在这两个表格之间实现专门的协调滚动。我将前面的表格(在视图层次结构中)设置为isUserInteractionEnabled = false
,并在后一个表格上添加了一个scrollViewDidScroll()
方法。前面的表格会忽略所有手势,因此任何平移手势/滚动都会触发后面的表格。随着滚动的发生,我的scrollViewDidScroll()
方法会被调用,然后我会检查contentOffset
并根据需要滚动(设置contentOffset
)另一个表格视图。这个方法效果很好。
直到......设计中添加了前表格视图上的按钮。我不能再使用isUserInteractionEnabled = false
,因为这会忽略点击,按钮将无法工作。我需要允许点击,但不响应平移(滚动)。我以为这会很容易,但要么我漏掉了一些简单的东西,要么它比我预期的要复杂。
我尝试过:
- 在前表格上禁用滚动(平移手势被忽略,但不会传递到后面的表格)
- 在前表格中重写平移手势,并将偏移应用到后面的表格(惯性不起作用)
- 将后表格的
UIPanGestureRecognizer
设置到前表格上(手势识别器只能属于一个视图) - 对前表格进行子类化,并重写
gestureRecognizerShouldBegin
以返回平移手势的false
(平移手势不会传递到后面的表格) - 对前表格进行子类化,并重写
touchesBegan
(moved、ended等)并在后表格上调用方法(没有效果) - 还尝试了其他一些方法
我对此束手无策。感谢您的帮助。
注:图片来自一个用于概念验证的简化代码。实际代码会根据底部/后面的表格中可见的内容更改顶部/前面的表格的contentOffset
。
英文:
I've got two overlapping UITableView
s with only vertical scrolling. I want specialized coordinated scrolling across the two tables. I took the front table (in the view layering) and set isUserInteractionEnabled = false
and added a scrollViewDidScroll()
method to the back one. The front table ignore all gestures, so any pan gesture/scrolling would trigger on the back table. As scrolling occurred my scrollViewDidScroll()
method would get called and I would check the contentOffset
and scroll (set contentOffset
) on the other table view as needed. It worked great.
Until.... the design added buttons to the front table view. I can no longer use isUserInteractionEnabled = false
because this ignores taps and buttons won't work. I need allow for taps, but not respond to pans(scrolling). I thought it would be fairly easily, but I'm either missing something simple or it's more complicated than I would expect.
I've tried:
- Disabling scrolling on the front table (pan gestures get ignored, but don't fall through to the back table)
- Overriding the pan gesture in the front table and applying the offset to the back table (momentum doesn't work properly)
- setting the
UIPanGestureRecognizer
from the back table on the front one (gesture recognizers can only belong to one view) - sub-classing the front table and doing an
override
ofgestureRecognizerShouldBegin
to returnfalse
for pan gestures (pan gestures don't fall through to the back table) - sub-classing the front table and doing an
overrider
fortouchesBegan
(moved, ended, etc) and calling the methods on the back table (no effect) - and a few other things
I'm stumped on this. Help appreciated.
Edit: The image is from a simplified bit of code for a proof of concept. The real code has the top/front table changes contentOffset
table height dependent on what's visible in the bottom/back table.
答案1
得分: 1
以下是代码的翻译部分:
这是两个表视图不重叠的快速示例。
我们将为“顶部”和“底部”表视图使用相同的单元格类,只是为了确认我们可以在两个表格的单元格中点击按钮。
class CommonCell: UITableViewCell {
static let identifier: String = "commonCell"
// 按钮点击的闭包
var gotTap: ((UITableViewCell) -> ())?
var isTop: Bool = true { didSet {
var cfg = btn.configuration
cfg?.baseBackgroundColor = isTop ? .systemBlue : .systemRed
btn.configuration = cfg
}}
var btn: UIButton!
let theLabel: UILabel = UILabel()
var myString: String = "" {
didSet {
theLabel.text = myString + " Count: \(self.tapCount)"
}
}
var tapCount: Int = 0 {
didSet {
theLabel.text = myString + " Count: \(self.tapCount)"
}
}
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 {
var cfg = UIButton.Configuration.filled()
cfg.title = "Button"
btn = UIButton(configuration: cfg)
btn.addAction (
UIAction { _ in
self.tapCount += 1
// 调用闭包以便控制器可以更新数据源
self.gotTap?(self)
}, for: .touchUpInside
)
[theLabel, btn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(v)
}
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
theLabel.centerYAnchor.constraint(equalTo: g.centerYAnchor),
btn.leadingAnchor.constraint(equalTo: theLabel.trailingAnchor, constant: 0.0),
btn.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
btn.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
btn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])
}
}
class SynchedTablesVC: UIViewController, UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate {
let topTable: UITableView = UITableView()
let bottomTable: UITableView = UITableView()
let topHeight: CGFloat = 226.0
var tapCounts: [[Int]] = []
override func viewDidLoad() {
super.viewDidLoad()
tapCounts = Array(repeating: Array(repeating: 0, count: 25), count: 2)
[topTable, bottomTable].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
topTable.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
topTable.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
topTable.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
topTable.heightAnchor.constraint(equalToConstant: topHeight),
bottomTable.topAnchor.constraint(equalTo: topTable.bottomAnchor, constant: 0.0),
bottomTable.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
bottomTable.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
bottomTable.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])
topTable.register(CommonCell.self, forCellReuseIdentifier: CommonCell.identifier)
bottomTable.register(CommonCell.self, forCellReuseIdentifier: CommonCell.identifier)
[topTable, bottomTable].forEach { v in
v.dataSource = self
v.delegate = self
}
// 底部表视图需要能够滚动到足够高以显示顶部表视图的底部行
bottomTable.contentInset.bottom = topHeight
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tapCounts[section].count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: CommonCell.identifier, for: indexPath) as! CommonCell
if tableView == topTable {
c.isTop = true
c.myString = "Top Row: \(indexPath.row)"
c.tapCount = tapCounts[0][indexPath.row]
// 按钮点击的闭包
c.gotTap = { [weak self] c in
guard let self = self,
let cell = c as? CommonCell,
let iPath = self.topTable.indexPath(for: cell)
else { return }
self.tapCounts[0][iPath.row] = cell.tapCount
}
c.contentView.backgroundColor = .init(red: 1.0, green: 0.75, blue: 0.75, alpha: 1.0)
} else {
c.isTop = false
c.myString = "Bottom Row: \(indexPath.row)"
c.tapCount = tapCounts[1][indexPath.row]
// 按钮点击的闭包
c.gotTap = { [weak self] c in
guard let self = self,
let cell = c as? CommonCell,
let iPath = self.bottomTable.indexPath(for: cell)
else { return }
self.tapCounts[1][iPath.row] = cell.tapCount
}
c.contentView.backgroundColor = .init(red: 0.75, green: 1.0, blue: 0.75, alpha: 1.0)
}
return c
}
// 跟踪滚动时要使用的表视图
var activeTableView: UITableView!
var inActiveTableView: UITableView!
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if let tblView = scrollView as? UITableView {
if tblView == topTable {
activeTableView = topTable
inActiveTableView = bottomTable
} else {
activeTableView = bottomTable
inActiveTableView = topTable
}
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let tblView = scrollView as? UITableView {
if tblView == activeTableView {
inActiveTableView.contentOffset.y = tblView.contentOffset.y
}
}
}
}
希望这对你有所帮助。如果需要进一步的翻译或帮助,请随时提问。
英文:
Here's a quick example of two table views - not overlapping each other.
We'll use the same cell class for both "top" and "bottom" table views, just so we can confirm that we can tap the buttons in the cells in both tables.
class CommonCell: UITableViewCell {
static let identifier: String = "commonCell"
// closure for button tap
var gotTap: ((UITableViewCell) -> ())?
var isTop: Bool = true { didSet {
var cfg = btn.configuration
cfg?.baseBackgroundColor = isTop ? .systemBlue : .systemRed
btn.configuration = cfg
}}
var btn: UIButton!
let theLabel: UILabel = UILabel()
var myString: String = "" {
didSet {
theLabel.text = myString + " Count: \(self.tapCount)"
}
}
var tapCount: Int = 0 {
didSet {
theLabel.text = myString + " Count: \(self.tapCount)"
}
}
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 {
var cfg = UIButton.Configuration.filled()
cfg.title = "Button"
btn = UIButton(configuration: cfg)
btn.addAction (
UIAction { _ in
self.tapCount += 1
// call the closure so the controller can
// update the data source
self.gotTap?(self)
}, for: .touchUpInside
)
[theLabel, btn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(v)
}
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
theLabel.centerYAnchor.constraint(equalTo: g.centerYAnchor),
btn.leadingAnchor.constraint(equalTo: theLabel.trailingAnchor, constant: 0.0),
btn.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
btn.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
btn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])
}
}
class SynchedTablesVC: UIViewController, UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate {
let topTable: UITableView = UITableView()
let bottomTable: UITableView = UITableView()
let topHeight: CGFloat = 226.0
var tapCounts: [[Int]] = []
override func viewDidLoad() {
super.viewDidLoad()
tapCounts = Array(repeating: Array(repeating: 0, count: 25), count: 2)
[topTable, bottomTable].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
topTable.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
topTable.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
topTable.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
topTable.heightAnchor.constraint(equalToConstant: topHeight),
bottomTable.topAnchor.constraint(equalTo: topTable.bottomAnchor, constant: 0.0),
bottomTable.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
bottomTable.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
bottomTable.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])
topTable.register(CommonCell.self, forCellReuseIdentifier: CommonCell.identifier)
bottomTable.register(CommonCell.self, forCellReuseIdentifier: CommonCell.identifier)
[topTable, bottomTable].forEach { v in
v.dataSource = self
v.delegate = self
}
// bottom table needs to be able to scroll up far enough
// to show bottom row of top table
bottomTable.contentInset.bottom = topHeight
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tapCounts[section].count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: CommonCell.identifier, for: indexPath) as! CommonCell
if tableView == topTable {
c.isTop = true
c.myString = "Top Row: \(indexPath.row)"
c.tapCount = tapCounts[0][indexPath.row]
// closure for button tap
c.gotTap = { [weak self] c in
guard let self = self,
let cell = c as? CommonCell,
let iPath = self.topTable.indexPath(for: cell)
else { return }
self.tapCounts[0][iPath.row] = cell.tapCount
}
c.contentView.backgroundColor = .init(red: 1.0, green: 0.75, blue: 0.75, alpha: 1.0)
} else {
c.isTop = false
c.myString = "Bottom Row: \(indexPath.row)"
c.tapCount = tapCounts[1][indexPath.row]
// closure for button tap
c.gotTap = { [weak self] c in
guard let self = self,
let cell = c as? CommonCell,
let iPath = self.bottomTable.indexPath(for: cell)
else { return }
self.tapCounts[1][iPath.row] = cell.tapCount
}
c.contentView.backgroundColor = .init(red: 0.75, green: 1.0, blue: 0.75, alpha: 1.0)
}
return c
}
// track which table view to use for content offset when scrolling
var activeTableView: UITableView!
var inActiveTableView: UITableView!
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if let tblView = scrollView as? UITableView {
if tblView == topTable {
activeTableView = topTable
inActiveTableView = bottomTable
} else {
activeTableView = bottomTable
inActiveTableView = topTable
}
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let tblView = scrollView as? UITableView {
if tblView == activeTableView {
inActiveTableView.contentOffset.y = tblView.contentOffset.y
}
}
}
}
Looks like this:
And, animation capture (too big to post here): https://imgur.com/a/V1yqj70
答案2
得分: 0
尝试使用以下方法来禁止用户触摸和拖动顶部表格进行滚动,但仍允许他们点击按钮:
默认值为true,表示启用滚动。将该值设置为false将禁用滚动。
当滚动被禁用时,滚动视图不接受触摸事件;它会将它们转发到响应者链上。
英文:
So, if u don't wanna allow users to touch and drag the TOP table to scroll, but still allow them to tap on the button.
Try using this:
isScrollEnabled
>The default value is true, which indicates that scrolling is enabled. Setting the value to false disables scrolling.
>
>When scrolling is disabled, the scroll view doesn’t accept touch events; it forwards them up the responder chain.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论