alexxjk
alexxjk

Reputation: 1711

RxSwift and UICollectionView Header

I'm using RxSwift with my UIViewController that contains a UICollectionView. I have tried to add a header to my collection view but this is never called:

func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath)

Upvotes: 8

Views: 13403

Answers (3)

sergiy batsevych
sergiy batsevych

Reputation: 385

I had same problem. Solve it moving to RxCollectionViewSectionedReloadDataSource

This type of datasource has headers/footers. Standard one RxCollectionViewReactiveArrayDataSource doesn't have headers/footers.

Even in RxCocoa UICollectionView+Rx extension gives RxCollectionViewSectionedReloadDataSource as example.

Example Directly from UICollectionView+Rx:

     let dataSource = RxCollectionViewSectionedReloadDataSource<SectionModel<String, Double>>()

     let items = Observable.just([
         SectionModel(model: "First section", items: [
             1.0,
             2.0,
             3.0
         ]),
         SectionModel(model: "Second section", items: [
             1.0,
             2.0,
             3.0
         ]),
         SectionModel(model: "Third section", items: [
             1.0,
             2.0,
             3.0
         ])
     ])

     dataSource.configureCell = { (dataSource, cv, indexPath, element) in
         let cell = cv.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! NumberCell
         cell.value?.text = "\(element) @ row \(indexPath.row)"
         return cell
     }

     items
        .bind(to: collectionView.rx.items(dataSource: dataSource))
        .disposed(by: disposeBag)

Upvotes: 6

Brooks DuBois
Brooks DuBois

Reputation: 725

After a long time figuring it out I have a complete answer. The key part depends on the RxDataSources dataSource.configureCell and dataSource.supplementaryViewFactory.

You will need to setup by registering your nibs normally. To make the collection view show it's header you will need to define a header size. Using the collection flow layout allows you preserve any bindings you might have inside cells for clickhandlers and not override the rx delegate proxy RxCocoa is using internally.

In this example I have one viewModel for the whole screen which holds an RxVariable with an array of itemViewModels for each item in the collection view.

UICollectionView with headers using RxSwift, RxDataSources, and RxCocoa:

var disposeBag: DisposeBag? = DisposeBag()

@IBOutlet weak var collectionView: UICollectionView!

let dataSource = RxCollectionViewSectionedReloadDataSource<SectionModel<String, ItemViewModel>>()
 
override func viewDidLoad() {
    super.viewDidLoad()
    registerHeaderCell()
    registerCollectionViewCell()
    setupLayout()
    bindViewModelToCollectionView()
}

private func registerHeaderCell() {
    let nib = UINib(nibName: "HeaderCell", bundle: nil)
    collectionView.register(nib, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderReuseId")
}

private func registerCollectionViewCell() {
    let nib = UINib(nibName:"CollectionViewCell", bundle: nil)
    collectionView.register(nib, forCellWithReuseIdentifier: "CellReuseId")
}

private func setupLayout(){
    let layout = UICollectionViewFlowLayout()
    layout.headerReferenceSize = CGSize(width: UIScreen.main.bounds.width, height: 100)
    collectionView.setCollectionViewLayout(layout, animated: false)
}

private func bindViewModelToCollectionView(){
    dataSource.supplementaryViewFactory = {(dataSource, collectionView, kind, indexPath) -> UICollectionReusableView in
        let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "HeaderReuseId", for: indexPath) as! HeaderCell
        header.setup()
        return header
    }
    
    dataSource.configureCell = {(datasource, collectionView, indexPath, itemViewModel) -> UICollectionViewCell in
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CellReuseId", for: indexPath) as! CollectionViewCell
        cell.setup()
        return cell
    } 

    viewModel.itemViewModels.asObservable().map { 
        (itemViewModels) -> [SectionModel<String, ItemViewModel>] in
       //my particular app has one big section but you can use this for mutiple sections like Sergiy's answer   
         return [SectionModel(model: "", items: itemViewModels)]
    }.bind(to: collectionView.rx.items(dataSource: dataSource))
    .addDisposableTo(disposeBag!)
}

deinit {
    disposeBag = nil
}

Upvotes: 9

solidcell
solidcell

Reputation: 7739

It has nothing to do with RxSwift, unless you're using RxDataSources, that is.

You just aren't setting up your UICollectionView correctly. Among other things:

  • Make sure you define func numberOfSections(in collectionView: UICollectionView) -> Int to return something greater than 0.
  • Make sure you define func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize to return a size that's bigger than CGSize(width: 0, height: 0).
  • If you're using a nib, then register it with func register(_ nib: UINib?, forSupplementaryViewOfKind kind: String, withReuseIdentifier identifier: String). Or, if you're creating it programmatically, then register it with func register(_ viewClass: AnyClass?, forSupplementaryViewOfKind elementKind: String, withReuseIdentifier identifier: String).

Upvotes: -3

Related Questions