UICollectionView状态恢复:自定义滚动位置

UICollectionView state restoration: customizing scroll position

我正在尝试找到处理元素可能四处移动的 UICollectionView 状态恢复的最佳方法。 我的目标是确保在重新启动应用程序时集合视图中最后查看的项目仍然可见,即使这些项目已经四处移动。例如,项目 A 在单元格中当应用程序被杀死时索引 3,当应用程序重新启动时如果模型说项目 A 应该显示在索引 4 处,我希望集合视图初始化偏移量到索引 4 处的单元格。

我认为在我的 UICollectionViewDataSource class 中实施 UIDataSourceModelAssociation 协议会为我解决这个问题,正如 documentation 所述:

[UITableView and UICollectionView] classes use the methods of this protocol to ensure that the same data objects (and not just the same row indexes) are scrolled into view and selected.

但是,我观察到实施此协议确实会在恢复期间正确影响 选定单元格 的 indexPath(这对我的应用程序并不重要),但不影响滚动位置。滚动位置(集合视图的 contentOffset)始终准确恢复到应用程序被终止时的位置,不受 UICollectionViewDataSource 的影响。

我确实有一个看起来像这样的解决方法。它与模型关联协议的模式基本相同,但我必须手动完成:

override func encodeRestorableStateWithCoder(coder: NSCoder) {
    let identifier = determineIdOfCurrentlyVisibleCell()
    coder.encodeObject(identifier, forKey: "visibleCellIdentifier")
}

override func decodeRestorableStateWithCoder(coder: NSCoder) {
    if let identifier = coder.decodeObjectForKey("visibleCellIdentifier") as? String {
        if let indexPath = model.indexPathForIdentifier(identifier) {
            collectionView.scrollToItemAtIndexPath(indexPath, atScrollPosition: .CenteredVertically, animated: false)
        }
    }
}

我是不是理解错了UIDataSourceModelAssociation的用法?有错误吗?有没有更优雅或更正确的方法来让它工作?

正如您已经指出的那样,UIDataSourceModelAssociation 似乎无法恢复 UICollectionView 的可见偏移量,但仅适用于选定的项目。我尝试在 modelIdentifierForElementAtIndexPathindexPathForElementWithModelIdentifier 上设置断点并注意到它们仅在我选择一个单元格后被调用。如果我在后台运行我的应用程序之前清除了我的集合视图的选定单元格,那么 modelIdentifierForElementAtIndexPath 将不会被调用,但是一旦我将至少一个单元格设置为选定,它就会被调用。至少我可以证实你不是唯一看到这种行为的人。

我认为由于 UICollectionView 的不同性质,创建将可见单元格滚动到正确点的行为可能并不简单,但这显然没有反映在 Apple 的文档中。手动将标识符编码到布局的第一个可见单元格应该是一个不错的选择。我正在做的是将集合视图的滚动偏移量包装在 NSValue 中并恢复它:

var collectionView: UICollectionView?

// ...

override func encodeRestorableStateWithCoder(coder: NSCoder) {
    if let view = collectionView, offsetValue = NSValue(CGPoint: view.contentOffset) {
        coder.encodeObject(offsetValue, forKey: CollectionViewContentOffsetKey)
    }

    super.encodeRestorableStateWithCoder(coder)
}

override func decodeRestorableStateWithCoder(coder: NSCoder) {
    if let offsetValue = coder.decodeObjectForKey(CollectionViewContentOffsetKey) as? NSValue {
        collectionView?.setContentOffset(offsetValue.CGPointValue(), animated: false)
    }

    super.decodeRestorableStateWithCoder(coder)
}

根据@stepane 使用 CGPoint 的建议进行更新。

  override func encodeRestorableState(with coder: NSCoder) {
        super.encodeRestorableState(with: coder)
        coder.encode(collectionView.contentOffset, forKey: "CollectionViewContentOffset")
    }

    override func decodeRestorableState(with coder: NSCoder) {
        super.decodeRestorableState(with: coder)
        let offsetValue = coder.decodeObject(forKey: "CollectionViewOffset") as! CGPoint
        collectionView?.setContentOffset(offsetValue, animated: false)
    }