提问人:Nikhil Muskur 提问时间:10/14/2023 最后编辑:Nikhil Muskur 更新时间:10/18/2023 访问量:99
快速连续多次调用 scrollToItem(at:at:animated:) 会导致 UICollectionView 中出现滚动故障
Multiple calls to scrollToItem(at:at:animated:) in quick succession causes scrolling glitch in UICollectionView
问:
在我的设置中,我有一个具有芯片样式的水平集合视图,在初始加载时,默认情况下选择第一个芯片,并与芯片对应,进行 API 调用,并将其数据加载到正下方的垂直 UICollectionView 中。
在点击 UICollectionView 中的任何芯片时,将启动一个 API 调用,并使用 将所选单元格滚动到 collectionview 的中心。另外,我必须重新加载最后选择的和当前选择的indexPath,以指示选择了新单元格。scrollToItem(at: selectedChipIndexPath, at: .centerHorizontally , animated: true)
如果用户点击芯片,查看下面加载的数据,然后点击下一个芯片,一切正常。
但是,如果用户快速点击连续的芯片,然后滚动动画有时会出现故障,从而导致不愉快的用户体验。scrollToItem(at: selectedChipIndexPath, at: .centerHorizontally , animated: true)
这是我的代码:
horizontalCollectionView.reloadItems(at: [lastSelectedIndexPath, selectedChipIndexPath])
// API Call
/*
This is an async call, which ones finishes informs the ViewController to the update it's vertical collection view with new data
*/
viewModel.fetchProductData(forPositon: selectedChipIndexPath)
DispatchQueue.main. asyncAfter ( .now() + .seconds(0.4)) {
horizontalCollectionView.scrollToItem(at: selectedChipIndexPath, at: .centerHorizontally , animated: true)
}
如果我不将滚动延迟几毫秒,那么集合视图的滚动动画就会开始出现非常严重的故障。
因此,任何人都可以为我指出正确的方向,说明如何处理或排队多个呼叫,以便我的 collectionView 滚动动画不会出现故障。scrollToItem(at:at:animated:)
我确实尝试过批量更新,但没有用。scrollToItem(at:at:animated:)
更新1:
以下是我的 UI 大部分外观
水平集合视图:适用于产品类别
垂直收藏视图:用于产品本身
更新2:
DonMag 提供的解决方案有效,但它引入了另一个问题,根据我的设计,我必须在选择时将芯片内 UILabel 的字体类型从常规更改为粗体,这样做会导致标签框架大小增加,对于大多数文本,标签被剪裁,因为我只是在操作视图而不是重新加载单元格本身。
我试图使 CollectionView 流布局失效。但它再次给我带来了同样的滚动故障问题。
答:
在没有看到完整代码的情况下,问题可能是由您反复调用.reloadItems(...)
如果这样做只是为了更改所选单元格的外观,则没有必要。
相反,让集合视图跟踪所选单元格(默认情况下会这样做),并让单元格根据其所选状态修改自己的外观。
例如,如果我们以这样的典型方式创建一个带有单个标签的单元格:
class SomeCell: UICollectionViewCell {
static let identifier: String = "SomeCell"
let theLabel: UILabel = {
let v = UILabel()
v.textAlignment = .center
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
theLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(theLabel)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
theLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0),
theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
theLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
theLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0),
])
// default "un-selected" appearance
contentView.layer.borderColor = UIColor(white: 0.9, alpha: 1.0).cgColor
contentView.layer.borderWidth = 1.0
}
override func layoutSubviews() {
contentView.layer.cornerRadius = contentView.bounds.height * 0.5
}
// the collection view will tell us whether the cell is selected or not
// so update the cell appearance here
override var isSelected: Bool {
didSet {
contentView.backgroundColor = isSelected ? UIColor(red: 0.5, green: 1.0, blue: 0.5, alpha: 1.0) : .systemBackground
contentView.layer.borderWidth = isSelected ? 0.0 : 1.0
}
}
}
我们已经覆盖了......集合视图将在需要时设置该属性,现在我们的单元格会自动更新其“已选择/未选择”外观。var isSelected: Bool
无需致电.reloadItems
使用上述单元格的水平集合视图的示例视图控制器 - 请注意,我正在创建 4 个示例标签的“重复集”(前缀为 1-4),因此我们有足够的单元格可供滚动:
class SomeViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
var collectionView: UICollectionView!
var sampleTags: [String] = [
"Pomogranate",
"Banana, Guava, Sapota",
"Oranges, Mosambi",
"Apple",
"Blueberry",
"Pineapple",
"Strawberry",
]
var tagsData: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
// let's create several sets of the sample tags
// so we have plenty of cells to scroll
for i in 1...4 {
sampleTags.forEach { s in
tagsData.append("\(i): \(s)")
}
}
let fl = UICollectionViewFlowLayout()
fl.scrollDirection = .horizontal
fl.estimatedItemSize = .init(width: 80.0, height: 40.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.heightAnchor.constraint(equalToConstant: 50.0),
])
collectionView.register(SomeCell.self, forCellWithReuseIdentifier: SomeCell.identifier)
collectionView.dataSource = self
collectionView.delegate = self
collectionView.layer.borderColor = UIColor.red.cgColor
collectionView.layer.borderWidth = 1.0
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return tagsData.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let c = collectionView.dequeueReusableCell(withReuseIdentifier: SomeCell.identifier, for: indexPath) as! SomeCell
c.theLabel.text = tagsData[indexPath.item]
return c
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// scroll the selected cell to the center
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
DispatchQueue.main.async {
self.viewModel.fetchProductData(forPositon: indexPath)
}
}
}
编辑 - 附加要求:选择时单元格字体更改为粗体,而不会导致单元格大小更改...
最简单的方法是向单元格添加两个标签 - 一个使用粗体字体,另一个使用常规字体。
设置约束条件,使“粗体”标签控制宽度,而“常规”标签的宽度将略高于所需宽度。
选中时显示粗体标签,选中时显示常规标签。
因此,从上面略微修改了单元格类:
class SomeCell: UICollectionViewCell {
static let identifier: String = "SomeCell"
public var title: String = "" {
didSet {
theLabel.text = title
theBoldLabel.text = title
}
}
private let theLabel = UILabel()
private let theBoldLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() -> Void {
[theLabel, theBoldLabel].forEach { v in
v.textAlignment = .center
v.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(v)
}
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
theBoldLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0),
theBoldLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
theBoldLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
theBoldLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0),
theLabel.topAnchor.constraint(equalTo: theBoldLabel.topAnchor, constant: 0.0),
theLabel.leadingAnchor.constraint(equalTo: theBoldLabel.leadingAnchor, constant: 0.0),
theLabel.trailingAnchor.constraint(equalTo: theBoldLabel.trailingAnchor, constant: 0.0),
theLabel.bottomAnchor.constraint(equalTo: theBoldLabel.bottomAnchor, constant: 0.0),
])
theLabel.font = .systemFont(ofSize: 17.0, weight: .regular)
theBoldLabel.font = .systemFont(ofSize: 17.0, weight: .bold)
// not strictly necessary, but the bold label controls the sizing
// so let's make sure it sizes correctly
theBoldLabel.setContentHuggingPriority(.required, for: .horizontal)
theBoldLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
// default "un-selected" appearance
contentView.layer.borderColor = UIColor(white: 0.9, alpha: 1.0).cgColor
contentView.layer.borderWidth = 1.0
theBoldLabel.isHidden = true
}
// the collection view will tell us whether the cell is selected or not
// so update the cell appearance here
override var isSelected: Bool {
didSet {
contentView.backgroundColor = isSelected ? UIColor(red: 0.5, green: 1.0, blue: 0.5, alpha: 1.0) : .systemBackground
contentView.layer.borderWidth = isSelected ? 0.0 : 1.0
theLabel.isHidden = isSelected
theBoldLabel.isHidden = !isSelected
}
}
override func layoutSubviews() {
contentView.layer.cornerRadius = contentView.bounds.height * 0.5
}
}
然后在控制器的 中,我们将设置单元格的属性,而不是设置“单个”标签的文本,这将在两个标签中设置相同的文本:cellForItemAt
title
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let c = collectionView.dequeueReusableCell(withReuseIdentifier: SomeCell.identifier, for: indexPath) as! SomeCell
//c.theLabel.text = tagsData[indexPath.item]
c.title = tagsData[indexPath.item]
return c
}
编辑 2 - 回应有关以编程方式选择的评论...
首先,不要调用:
cell.isSelected = true
这并不能告诉集合视图单元格已被选中。
这是以编程方式选择单元格的正确方法(在本例中,我们想要选择单元格/项目并使其滚动到中心):5
collectionView.selectItem(at: IndexPath(item: 5, section: 0), animated: true, scrollPosition: .centeredHorizontally)
请注意,以编程方式选择单元格不会调用委托函数。调用它是为了让您知道用户选择了一个单元格。didSelectItemAt
因此,如果您想以编程方式选择一个单元格,并且希望发生一些事情,请使用“执行某项操作”代码来跟进它。
例如:
let idxPath: IndexPath = IndexPath(item: 5, section: 0)
collectionView.selectItem(at: idxPath, animated: true, scrollPosition: .centeredHorizontally)
DispatchQueue.main.async {
self.viewModel.fetchProductData(forPositon: idxPath)
}
评论
cell.isSelected = true; collectionView.selectItem(at: indexPath)
评论
didSelectItemAt
scrollToItem