UICollectionView(自动大小).reloadData() 会将 contentOffset 重置为 0(顶部)。

21
我有一个带有标准 UICollectionViewFlowLayoutUICollectionView,其中 estimatedItemSize 设置为 UICollectionViewFlowLayoutAutomaticSize
一旦我调用 collectionView.reloadData(),视图的滚动位置将重置为 CGPoint(x:0,y:0)。我知道应该只在必要时使用 .reloadData(),但在我的情况下是必要的。到目前为止,我发现这可能是由于未通过 collectionView(_:layout:sizeForItemAt :) 返回项目大小引起的。(如果有类似于 UITableViewtableView:estimatedHeightForRowAtIndexPath:的解决方案,详见 此 StackOverflow 答案 将不胜感激)。

你能否提供更多关于你想要实现的细节?我无法理解你最终的结果。听起来你可能需要使用自定义的UICollectionViewFlowLayout覆盖collectionViewContentSize以阻止偏移量的重置。 - luke
1
{btsdaf} - Maddiee
也许你可以尝试调用"invalidateLayout"然后重新加载。因为重新加载不应该被视为最后的手段。否则你将无法重新加载全部数据。 - Maddiee
reloadData() 也会重新加载界面,因此也许可以在重新加载之前存储集合的位置,并在重新加载后手动设置它? - Bruno Fulber Wide
你好OP,你解决了这个问题吗?我目前也遇到了这个问题。 - Rakesha Shastri
有什么解决办法吗?我也遇到了同样的问题。 - axunic
5个回答

3

我曾经遇到过同样的问题,通过子类化集合视图并覆盖reloadData方法来暂时存储当前的contentOffset,然后监听contentOffset的变化,如果我有任何存储在我的临时偏移变量中的内容,则将其更改回来。

class MyCollectionView : UICollectionView {
    deinit {
        removeObserver(self, forKeyPath: #keyPath(UIScrollView.contentOffset))
    }

    override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
        setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    private func setup() {
        self.addObserver(self, forKeyPath: #keyPath(UIScrollView.contentOffset), options: [.old, .new], context: nil)
    }

    private var temporaryOffsetOverride : CGPoint?
    override func reloadData() {
        if let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout, flowLayout.estimatedItemSize == UICollectionViewFlowLayout.automaticSize {
            temporaryOffsetOverride = contentOffset
        }
        super.reloadData()
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == #keyPath(UIScrollView.contentOffset) {
            if let offset = temporaryOffsetOverride {
                temporaryOffsetOverride = nil
                self.setContentOffset(offset, animated: false)
            }
        }
    }
}

3
在我的情况下,我解决了这个问题: 在这里我检测我的单元格是否完全可见,如果不可见-移动到它。(无需“跳跃”)
 let cellRect = collectionView.convert(cell.frame, to: collectionView.superview)
      if !collectionView.frame.contains(cellRect) {
        collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
        return collectionView.reloadData()
      }

这里,我保存了我的contentOffset:

   let currentContentOffSet = collectionView.contentOffset
         if #available(iOS 12.0, *) {
            UIView.animate(withDuration: 0) {
               collectionView.reloadData()
               collectionView.performBatchUpdates(nil, completion: { _ in
                  collectionView.contentOffset = currentContentOffSet
               })
            }
         } else {
            UIView.animate(withDuration: 0) {
               collectionView.reloadItems(at: [self.lastSelectedIndexPath, indexPath])
               collectionView.contentOffset = currentContentOffSet
            }
         }

如果单元格最近变为可见状态(或已经可见),并且我点击该单元格,则可以使用它来移动到单元格。同时避免“跳动”(不重置contentOffset)。

在collectionViewController中,您也可以尝试这样做:

  func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
      guard let cell = collectionView.cellForItem(at: indexPath) else { return }
      if lastSelectedIndexPath != indexPath {
         let cellRect = collectionView.convert(cell.frame, to: collectionView.superview)
         if !collectionView.frame.contains(cellRect) {
            collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
         } else {
            collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .init())
         }
         collectionView.deselectItem(at: lastSelectedIndexPath, animated: false)
         lastSelectedIndexPath = indexPath
      }
   }

这是在collectionViewCellModel中:

  override var isSelected: Bool {
      didSet {
         switch self.isSelected {
         case false:

         case true:

         }
      }
   }

+1. 对于我来说,使用performBatchUpdates完成处理程序是唯一可靠的方法,在重新加载数据后立即设置contentOffset - markckim

1
重新加载UICollectionView的部分对我很有效。
UIView.performWithoutAnimation {self.collStages.reloadSections([0])}
collStages.scrollToItem(at: IndexPath(row: index, section: 0), at: .centeredHorizontally, animated: true)

0

reloadData重新加载你的UICollectionView中的所有单元格,所以如果你有任何对单元格的引用,它将指向一个不存在的对象,而且更重要的是,如果它是强引用,可能会导致循环引用。所以:

你不应该这样做!

但是...

很难想象出需要使用reloadData而无法通过插入和删除对象来替代的情况。

所以你可能想要使用的是重新加载单个行或者在你的数据模型中插入变化的项目。你没有详细说明你的问题是什么,但你可以执行之前提到的任何操作,比如:

        collectionView.performBatchUpdates({
            collectionView.insertItems(at: <#T##[IndexPath]#>)
            collectionView.deleteItems(at: <#T##[IndexPath]#>)
            collectionView.reloadItems(at: <#T##[IndexPath]#>)
        }, completion: { (<#Bool#>) in
            <#code#>
        })

但是请确保您首先编辑数据模型。所以:

var dataModel: [String] = ["0","1","2"]   //Data model for Collection View
collectionView.reloadData() // In that point you will have 3 cells visible on the sceen
.
.
.
dataModel.removeFirst() // now your data model is ["1","2"]
collectionView.performBatchUpdates({
    collectionView.deleteItems(at: [IndexPath(row: 0, section:0)])
}, completion: { success in 
    //some check consistency code
})

有了这个,你的collectionview将保持不变,除了第一行之外不会做出任何其他更改。


-1

不确定是否有效,但你可以试一试:

    let currentOffset = self.collectionView.contentOffset
    self.collectionView.reloadData()
    self.collectionView.setContentOffset(currentOffset, animated: false)

请告诉我。谢谢 :)


使用了上面的代码,但它不起作用,视图的滚动位置重置为CGPoint(x:0,y:0)。 - Ankit Kushwah
你不确定如何使用它,reloadData()之前的contentOffset的值是多少?你尝试打印过吗? - Maddiee

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接