swiftuikitrx-swiftcollectionview

How to set collectionView sizeForItemAt from Observable RxSwift


I have some issues when trying to make the height of my collection view cells dynamic. The height data is inside my enum Observable<[MovieSection]> array. As far as I know, RxSwift doesn't provide a sizeForItemAt delegate function.

So, how can I pass my Observable<[MovieSection]> array to the 'sizeForItemAt' delegate so that I can switch my enum based on its case and retrieve the height?

enum MovieSection {
    case header(height: Double, movie: Movie)
    case horizontal(title: String, height: Double, items: [Movie])
}

class ViewModel {
   private var _sections = BehaviorRelay<[MovieSection]>(value: [])
   
   var sections: Observable<[MovieSection]>? {
       return _sections.asObservable()
   }


   func getData() {
     //items and movie from somewhere i get from network/mock

     let sectionsData: [MovieSection] = [
           .header(height: 270, movie: popular[1]),
           .horizontal(title: "Popular", height: 230, items: popular),
           .horizontal(title: "Top Rated", height: 230, items: topRated)
      ]
      _sections.accept(sectionsData)
   }
}


class HomeViewController: UIViewController {
   var viewModel = ViewModel()
   @IBOutlet weak var collectionView: UICollectionView!


    override func viewDidLoad() {
        super.viewDidLoad()

        setupBindings()
        viewModel.getData()
    }

   private func setupBindings() {
        viewModel.sections
            .bind(to: collectionView.rx.items) { collectionView, row, item in
                let indexPath = IndexPath(row: row, section: 0)
                
                switch item {
                case .header(_, let movie):
                    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HeaderSectionCell.identifier, for: indexPath) as! HeaderSectionCell
                    cell.configure(with: movie.backdropPath)
                    return cell
                case .horizontal(let title, _, let movies):
                    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HorizontalMoviesCell.identifier, for: indexPath) as! HorizontalMoviesCell
                    cell.configure(with: title, items: movies)
                    return cell
                }
            }
            .disposed(by: bag)
        
        collectionView.rx.setDelegate(self)
            .disposed(by: bag)
    }

}

//MARK: UICollectionView Delegate
extension HomeViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

        //Here i want pass my height from my [MovieSection] array of enum so i can access my height

        return CGSize(width: collectionView.frame.width, height: 230)
    }
}

What I've done before is directly accessing the value from the BehaviorRelay, so I can directly access the array without returning new Observable variable

like this:

func getSectionsValue() -> [MovieSection] {
    return _sections.value
}

My expecting to get access from my Observable<[MovieSection]> in sizeForItemAt CollectionView so i can access my height


Solution

  • I've always used constraints to set the height of cells. I suggest you add a height constraint on the cells' content views and pass the height into the cell just like you do the other configuration data.

    However, if you want to continue with using collectionView(_:layout:sizeForItemAt:) then a BehaviorRelay is required.

    The simplest method is to add one to the view controller and bind it to the sections observable...

    let heightForItemAt = BehaviorRelay<[CGFloat]>(value: [])
    
    func setupBindings() { 
        viewModel.sections
            .map { $0.map { $0.height } }
            .bind(to: heightForItemAt)
            .disposed(by: bag)
        // the rest of the method as you have it...
    }
    

    The above assumes you have this:

    extension MovieSection {
        var height: Double {
            switch self {
            case .header(let height, _), .horizontal(_, let height, _):
                return height
            }
        }
    }
    

    Now in your delegate method, you can access the heightForItemAt relay's value to get the height.


    If you want you could follow my article Convert a Swift Delegate to RxSwift Observables. Then you could do it like this:

    viewModel.sections
        .map { [collectionView] sections in
            sections.map { CGSize(width: collectionView!.frame.width, height: $0.height) }
        }
        .bind(to: collectionView.rx.sizeForItemAt)
        .disposed(by: bag)
    

    But it still requires a BehaviorSubject (inside the delegate proxy.)