快速连续多次调用 scrollToItem(at:at:animated:) 会导致 UICollectionView 中出现滚动故障

Multiple calls to scrollToItem(at:at:animated:) in quick succession causes scrolling glitch in UICollectionView

提问人:Nikhil Muskur 提问时间:10/14/2023 最后编辑:Nikhil Muskur 更新时间:10/18/2023 访问量:99

问:

在我的设置中,我有一个具有芯片样式的水平集合视图,在初始加载时,默认情况下选择第一个芯片,并与芯片对应,进行 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 大部分外观

水平集合视图:适用于产品类别

垂直收藏视图:用于产品本身

Visual Representation on how my UI looks

更新2:

DonMag 提供的解决方案有效,但它引入了另一个问题,根据我的设计,我必须在选择时将芯片内 UILabel 的字体类型从常规更改为粗体,这样做会导致标签框架大小增加,对于大多数文本,标签被剪裁,因为我只是在操作视图而不是重新加载单元格本身。

我试图使 CollectionView 流布局失效。但它再次给我带来了同样的滚动故障问题。

iOS Swift UIKiter UIColictionViewCell

评论

0赞 DonMag 10/15/2023
什么是“芯片风格”?为什么要重新加载上次选择的和当前选择的 indexPath?目前尚不清楚你在用你的“API 调用”做什么——在 中,你是否在进行“API 调用”......等待它完成......然后重新加载您的手机并呼叫?如果是这样,问题是用户在此过程中选择了不同的项目吗?didSelectItemAtscrollToItem
0赞 Nikhil Muskur 10/15/2023
@DonMag 使用示例 UI 更新了我的问题。我正在更新最后和当前选择的 IndexPath 以供选择(传递一个布尔值以指示已选择和未选择的状态)。API 调用是异步的,一旦它获取数据,viewModel 就会使用 2 路绑定通知 VC 以更新其 UI。一旦选择了任何筹码,我就会将其滚动到中心,然后快速连续地点击连续的筹码会导致滚动动画出现故障。

答:

1赞 DonMag 10/16/2023 #1

在没有看到完整代码的情况下,问题可能是由您反复调用.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
    }
}

然后在控制器的 中,我们将设置单元格的属性,而不是设置“单个”标签的文本,这将在两个标签中设置相同的文本:cellForItemAttitle

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)
}

评论

0赞 Nikhil Muskur 10/16/2023
感谢@DonMag的指导,我将尝试实现这一点并尽快报告。
0赞 Nikhil Muskur 10/17/2023
上面的答案解决了我的问题,但它引入了另一个问题。选中时,内部芯片的 UILabel 会从“常规字体”更改为“粗体字体”,从而增加其大小,从而导致文本剪辑。为了解决这个问题,我试图使流程布局无效,但它再次给我带来了同样的滚动故障问题。早些时候,由于我重新加载单元格,因此没有发生此问题,但现在由于没有重新加载此问题。
0赞 DonMag 10/17/2023
@NikhilMuskur - 在发布问题时提供完整的信息会有所帮助。您是否希望在字体加粗时更改“芯片”宽度?或者,您是否想要一致的宽度,并且只是更改字体?
0赞 DonMag 10/17/2023
@NikhilMuskur - 我在答案中添加了编辑...看看这是否是你需要的。
0赞 Nikhil Muskur 10/18/2023
感谢您提供更新的答案,这解决了我的疑问和问题。有关如何以编程方式使用上述技术选择单元格的任何提示。我试过了,但这没有用cell.isSelected = true; collectionView.selectItem(at: indexPath)