Wiingaard
Wiingaard

Reputation: 4302

Get model from UICollectionView indexpath

I'm using RxSwift to bind a model array to a collection view

How do I get the model object from a given indexPath?

I'm doing the binding like this:

vm.bikeIssueCatagories()
        .drive(self.collectionView.rx.items(cellIdentifier: "BikeIssueCategoryCollectionViewCell", cellType: UICollectionViewCell.self)) { row, data, cell in
        }.disposed(by: disposeBag)

The core of my issue is, that I need to get both the model object and the cell that a user selects. Using collectionView.rx.modelSelected(T.self) only gives me the model og type T. And calling collectionView.rx.itemSelected only gives me the selected IndexPath

collectionView.rx.itemSelected.asDriver()
        .driveNext { [unowned self] indexPath in
            guard let model = try? collectionView.rx.model(at: indexPath) else { return }
            guard let cell = self.collectionView.cellForItem(at: indexPath) else { return }
        }.disposed(by: disposeBag)

But this gives me an error when trying to the the model at indexPath:

Type 'inout UICollectionView' does not conform to protocol 'ReactiveCompatible'

Just trying:

let indexPath = IndexPath.init()
self.collectionView.rx.model(at: indexPath)

also gives me an error:

Ambiguous reference to member 'model(at:)'

SO... How to get both the model object and the cell that a user selects?

Upvotes: 4

Views: 3254

Answers (3)

headstream
headstream

Reputation: 226

Your self-accepted solution is not optimal, because the modelSelected stream maps indexPath internally and needs to be used when there is no need to know about indexPath. In your case, it is better to use use itemSelected and convert to a tuple for example.

 collectionView.rx.itemSelected
    .map { [weak self] indexPath in
            guard
               let model = try? self?.collectionView.rx.model(at: indexPath) as YourModelName?,
               let cell = self?.collectionView.cellForItem(at: indexPath)  
            else {
                preconditionFailure("Describing of error")
            }


            return (cell, model)
        }

Upvotes: 2

Wiingaard
Wiingaard

Reputation: 4302

I could have done like RamwiseMatt proposed. Making a method on my ViewModel that takes the IndexPath and return the model. I did however end up using a zip:

let modelSelected = collectionView.rx.modelSelected(SelectableBikeIssueCategory.self).asObservable()

let cellSelected = collectionView.rx.itemSelected.asObservable()
            .map { [weak self] indexPath -> UICollectionViewCell in
                guard let cell = self?.collectionView.cellForItem(at: indexPath) else { fatalError("Expected cell at indexpath") }
                return cell
            }

Observable.zip(cellSelected, modelSelected)
            .map { [weak self] cell, category -> SomeThing in
                return SomeThing()
            }

Upvotes: 6

RamwiseMatt
RamwiseMatt

Reputation: 2957

Your ViewModel defines a method bikeIssueCatagories(), which is what your UIViewController binds the UICollectionView to. To get your model at the correct position, you can use the itemSelected property you mentioned, which gives you an Observable<IndexPath>, which you should feed into your ViewModel. There, you can use the IndexPath's item property to determine which model from your data array (given in bikeIssueCatagories()) is the one you are looking for. The modelSelected property makes this easier for you since it already knows the datasource, so all you have to provide is the type of your Model (replace T.self with YourModelName.self.)

I'm not sure why you want a reference to your cell as well, but if you must, you can just use cellForItem(at:) on your UICollectionView (feeding it the IndexPath you obtained via itemSelected.)

Upvotes: 1

Related Questions