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
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.)