如何使用 RxSwift 在单元格添加/删除到 UITableView 后正确滚动

How to properly scroll after cells were added/removed to UITableView with RxSwift

提问人:MrKew 提问时间:7/25/2023 更新时间:7/25/2023 访问量:57

问:

我有一个带有一个部分的视图控制器,我使用RxSwift以以下方式显示其中。每当更改时,我想滚动到底部(或其他任何地方)。我正在将文件用于我的单元格和动画数据源。tableViewdatatableViewdata.xib

问题是,有时在调用时,单元格尚未显示在 中,并且调用会抛出错误。scrollToRowtableView

如何解决这个问题?

我唯一能想到的就是在数据更改和滚动之间设置延迟,但延迟应该有多大?此外,我认为有一些更强大的方法可以解决这个问题。不知何故等到单元格实际显示。

我试过做:

  • scrollRectToVisible(...)到底部tableView.contentSize
  • 滚动前检查是否data.count == tableView.numberOfRows(inSection: 0)

它们中的任何一个都会导致更新时有时不滚动,这比错误要好,但仍然不是预期的结果。tableViewdata

class SomeVC: UIViewController {
    private typealias DataSource = RxTableViewSectionedAnimatedDataSource<AnimatableSectionModel<..., ...>>

    private let db = DisposeBag()

    @IBOutlet weak var tableView: UITableView!

    private let data = BehaviorRelay<[...]>(value: ...)
    
    private lazy var dataSource = createDataSource()
    
    override func viewDidLoad() {
        
        tableView.register(UINib(nibName: ..., bundle: ...), forCellResuseIdentifier: ...)
        dataSource.animationConfiguration = AnimationConfiguration(insertAnimation: .bottom, reloadAnimation: .fade, deleteAnimation: .fade)

        data
            .asDriver()
            .do(afterNext: { [unowned self] data in
                DispatchQueue.main.async { [weak self] in
                    let lastIndexPath = getLastIndexPath(from: data)
                    // This does not always hold: data.count == tableView.numberOfRows(inSection: 0)
                    self?.tableView.scrollToRow(at: lastIndexPath, at: .none, animated: true)
                }
            }
            .map { [AnimatableSectionModel(...)] }
            .drive(tableView.rx.items(dataSource: dataSource))
            .disposed(by: db)
    }

    private func getLastIndexPath(from data: [...]) {
        // Something like this
        return IndexPath(row: data.count - 1, section: 0)
    }

    private func createDataSource() -> DataSource {
        return DataSource(configureCell: { dataSource, tv, indexPath, model in
            let cell = tv.dequeueReusableCell(withIdentifier: ..., for: indexPath)
            ...
            return cell
        }
    }
}
斯威夫特 uitableview rx-swift

评论

0赞 Daniel T. 7/27/2023
把这个断言放在 - 它曾经开火吗?afterNext:assert(sections[0].items.count == tableView!.numberOfRows(inSection: 0))

答:

1赞 Daniel T. 7/25/2023 #1

以下方法有效,但并不理想:

source
    .map { [SectionModel(model: "", items: $0)] }
    .bind(to: tableView.rx.items(dataSource: dataSource))
    .disposed(by: disposeBag)

source
    .filter { $0.count > 0 }
    .map { IndexPath(row: $0.count - 1, section: 0) }
    .bind(onNext: { [tableView] indexPath in
        tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
    })
    .disposed(by: disposeBag)

重要的一点,也是最大的问题,是这两个订阅取决于顺序。由于它们不是独立的,因此如果您反转它们,应用程序将无法运行。

这也对我有用。也许你计算错了最后一行?

source
    .map { [SectionModel(model: "", items: $0)] }
    .do(afterNext: { [tableView] sections in
        if sections[0].items.count > 0 {
            tableView.scrollToRow(at: IndexPath(row: sections[0].items.count - 1, section: 0), at: .bottom, animated: true)
        }
    })
    .bind(to: tableView.rx.items(dataSource: dataSource))
    .disposed(by: disposeBag)

评论

0赞 MrKew 7/27/2023
不,这绝对不适合我。它非常难以重现错误,它几乎每次都有效,但不是每次都有效。我确定我正确计算了索引路径(由 scrollRectToVisible 有时也不会滚动的事实支持)。你对为什么它不应该起作用有任何猜测吗?可能是 asDriver()、.xib 还是 AnimatedDataSource?当可观察的源中有多个更改时,似乎会发生这种情况