Reputation: 10517
I'm writing a tiny image viewer based on UICollectionView and RxSwift/RxCocoa to provide delegation stuff. My goal is to make it possible to jump to a certain cell right after data is loaded from viewModel. Here is what I've tried so far.
ViewModel:
class ImageViewerViewModel {
let dataSource = BehaviorRelay<[UIImage]>(value: [])
let selectedImageIndex = BehaviorSubject<Int>(value: 0)
private let disposeBag = DisposeBag()
init(imageCollection: [UIImage], selectedIndex: Int) {
var newEntries = [UIImage]()
newEntries.append(contentsOf: imageCollection)
dataSource.accept(newEntries)
selectedImageIndex.onNext(selectedIndex)
}
}
ViewController:
class ImageViewerViewController: UIViewController, UIScrollViewDelegate {
private func setUpBindings() {
guard let viewModel = viewModel else { return }
viewModel.selectedImageIndex
.subscribe(onNext: { [weak self] selectedImageIndex in
// expect UICollectionView already loaded its data but it raises out-of-bounds exception instead
self?.collectionView.scrollToItem(at: IndexPath(row: selectedImageIndex, section: 0), at: .left, animated: false)
})
.disposed(by: disposeBag)
collectionView.rx.setDelegate(self)
.disposed(by: disposeBag)
viewModel.dataSource
.bind(to: collectionView.rx.items) { [weak self]
(collectionView: UICollectionView, row: Int, image: UIImage) in
// returns UICollectionViewCell
}
.disposed(by: disposeBag)
}
}
I expected that after calling dataSource.accept
method UICollectionView
stored data somewhere internally (like it works with collectionView.reloadData
) and we can programmatically scroll to certain cell. It appears that's not so.
collectionView.scrollToItem
causes exception
Attempted to scroll the collection view to an out-of-bounds item (1) when there are only 0 items in section 0
How can I subscribe on some sort of onDataLoaded
observable?
Upvotes: 1
Views: 1093
Reputation: 33967
In effect, what you have done is set the delegate in viewDidLoad and then immediately called scrollToItem(at:at:animated:)
. If this was normal imperative code, you would have known intuitively that such a thing wouldn't work. How would you do this without Rx? The answer is here UICollectionView auto scroll to cell at IndexPath.
So how do you translate the accepted answer to the above question into RxSwift?
rx.methodInvoked(#selector(viewWillLayoutSubviews))
.take(1)
.withLatestFrom(viewModel.selectedImageIndex)
.subscribe(onNext: { [weak self] selectedImageIndex in
// now it should work
self?.collectionView.scrollToItem(at: IndexPath(row: selectedImageIndex, section: 0), at: .left, animated: false)
})
.disposed(by: disposeBag)
If you put the above in the setUpBindings method, it will wait until viewWillLayoutSubviews is called, then take whatever was last emitted from selectedImageIndex and scroll to it. It will only do this one time (because of the take(1)
) just like in the accepted answer of the above question all without having to track state manually.
Upvotes: 3