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

wlwcrazw  于 2023-10-21  发布在  iOS
关注(0)|答案(1)|浏览(125)

在我的设置中,我有一个带有芯片样式的水平集合视图,在初始加载时,默认情况下选择第一个芯片,并对芯片进行API调用,并将其数据加载到下面的垂直UICollectionView中。
在点击UICollectionView中的任何芯片时,将启动一个API调用,并使用scrollToItem(at: selectedChipIndexPath, at: .centerHorizontally , animated: true)将所选单元格滚动到collectionview的中心。另外,我必须重新加载上次选择的和当前选择的indexPath,以表明新单元格被选中。
如果用户点击芯片,查看下面加载的数据,然后点击下一个芯片,一切正常。
但是,如果用户快速点击连续的芯片,那么scrollToItem(at: selectedChipIndexPath, at: .centerHorizontally , animated: true)有时会出现滚动动画故障,导致不愉快的UX体验。
下面是我的代码:

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

如果我不延迟几毫秒的滚动,那么集合视图的滚动动画开始出现非常严重的故障。
因此,任何人都可以指出我在正确的方向,我如何处理或排队多个scrollToItem(at:at:animated:)调用,使我的收藏查看滚动动画不出故障。
我确实尝试在批量更新中 Package scrollToItem(at:at:animated:),但它不起作用。

更新1:

以下是我的UI的主要外观
水平集合视图:产品类别的
垂直集合视图:产品本身

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

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

e0bqpujr

e0bqpujr1#

如果没有看到完整的代码,问题可能是由于您重复调用.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...集合视图将在需要时设置该属性,现在我们的单元格自动更新其“selected / un-selected”外观。
无需调用.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属性,而不是设置“single”标签的文本,这将在两个标签中设置相同的文本:

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委托函数。调用它是为了让您知道 user 选择了一个单元格。
所以,如果你想以编程方式选择一个单元格,并且你想发生一些事情,那么就用那个“做一些事情”代码来跟踪它。
举例来说:

let idxPath: IndexPath = IndexPath(item: 5, section: 0)
collectionView.selectItem(at: idxPath, animated: true, scrollPosition: .centeredHorizontally)
DispatchQueue.main.async {
    self.viewModel.fetchProductData(forPositon: idxPath)
}

相关问题