Instagram / IGListKit

A data-driven UICollectionView framework for building fast and flexible lists.
https://instagram.github.io/IGListKit/
MIT License
12.87k stars 1.54k forks source link

Section Controllers with multiple cells, horizontally laid out #1210

Open benvigier opened 6 years ago

benvigier commented 6 years ago

New issue checklist

General information

Hi IG team,

First of all, thank you for sharing this framework, I've been using it extensively for a few months and it has simplified my life drastically. So far I've always been able to implement the UX I wanted by doing research and looking at the examples you provided, but I finally hit a wall this week.

As illustrated on the drawing below, I need to create a nested section that scrolls horizontally and hosts a bunch of section controllers, each containing 2 cells. What I am not managing to do is have the 2 cells be laid out next to each other horizontally instead of stacked on top of each other, in each (blue) section controller.

I am using IGListkit's ListCollectionViewLayout for the nested collection view, which allows me to have the blue section controllers nicely laid out horizontally as shown on the drawing, but cannot get the 2 cells not to be stacked on top of each other.

I am attaching a simplified version of the code, any guidance on how to this would be most appreciated.

Thanks again!

iglistkit issue illustration

Simplified code for the blue section controller

class BlueSectionController : ListSectionControllerBase{

  //Space between pods
  static let customInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
  static let sectionHeight : CGFloat = OrangeCell.cellHeight + BlueSectionController.customInsets.top + BlueSectionController.customInsets.bottom

  var viewModel : ViewModel?

  override init() {
    super.init()
    inset = BlueSectionController.customInsets
  }

  override func numberOfItems() -> Int {
    return 2
  }

  override func sizeForItem(at index: Int) -> CGSize {
    guard let context = collectionContext else {return .zero}

    switch index{
    case 0: //Orange cell (cell 1)
      return CGSize(width: OrangeCell.cellWidth, height: OrangeCell.cellHeight)
    default: //Green cell (cell 2)
      let width = context.containerSize.width - OrangeCell.cellWidth - 50 //50 = space on the right to allow next section to tip in
      return CGSize(width: width, height: GreenCell.cellHeight) //Both cells have the same height
    }
  }

  override func cellForItem(at index: Int) -> UICollectionViewCell {

    guard let context = collectionContext else {return UICollectionViewCell()}

    switch index{
    case 0: //Orange cell (cell 1)
      guard let cell = context.dequeueReusableCell(withNibName: "OrangeCell", bundle: nil, for: self, at: index) as? OrangeCell else {return UICollectionViewCell()}
      //Configure cell...
      return cell

    default: //Green cell (cell 2)
      guard let cell = context.dequeueReusableCell(withNibName: "GreenCell", bundle: nil, for: self, at: index) as? GreenCell else {return UICollectionViewCell()}
      //Configure cell...
      return cell
    }
  }

  //This function is used to hand an object to the section controller. Note this method will always be called before any of the cell protocol methods.
  override func didUpdate(to object: Any) {
    guard let viewModel = object as? ViewModel else {return}
    self.viewModel = viewModel
  }
}

Simplified code for the nested section controller

class NestedSectionController: ListSectionController{

  final lazy var listAdapter: ListAdapter = { [unowned self] _ in
    let adapterUpdater = ListAdapterUpdater()
    //adapterUpdater.delegate = self
    let adapter = ListAdapter(updater: adapterUpdater, viewController: self.viewController)
    return adapter
    }()

  //Space between this section and the next
  static let customInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
  var viewModelArray : [ViewModel]?
  var 

  override init() {
    super.init()
    listAdapter.dataSource = self
  }

  override func numberOfItems() -> Int {
    return 1
  }

  override func didUpdate(to object: Any) {
    guard let nestedSectionToken = object as? NestedSectionToken else { return }
    self.viewModelArray = nestedSectionToken.viewModelArray
  }

  override func didSelectItem(at index: Int) {
  }

  override func sizeForItem(at index: Int) -> CGSize {
    guard let context = collectionContext else {return .zero}
    return CGSize(width: context.containerSize.width, height: BlueSectionController.sectionHeight * 2)
  }

  override func cellForItem(at index: Int) -> UICollectionViewCell {
    guard let cell = collectionContext?.dequeueReusableCell(of: HorizontalEmbeddedCollectionViewCell.self, for: self, at: index) as? HorizontalEmbeddedCollectionViewCell else {return UICollectionViewCell()}
    listAdapter.collectionView = cell.collectionView
    listAdapter.scrollViewDelegate = self
    listAdapter.collectionView?.reloadData() //Needed to avoid crash (see https://github.com/Instagram/IGListKit/issues/952)
    return cell
  }
}

extension NestedSectionController: ListAdapterDataSource {

  func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
    return arrayOfViewModels as [ListDiffable]
  }

  func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
    guard let viewModel = object as? ViewModel else {return ErrorSectionController()}

    let section = BlueSectionController()
    //Configure section...
    return section
  }

  func emptyView(for listAdapter: ListAdapter) -> UIView? {
    return nil
  }

}

Code for the Embedded CollectionView cell

final class HorizontalEmbeddedCollectionViewCell: UICollectionViewCell {

  static let cellHeight : CGFloat = MyPodCellSectionController.sectionHeight

  lazy var collectionView: UICollectionView = {
    let layout = ListCollectionViewLayout(stickyHeaders: false, scrollDirection: .horizontal, topContentInset: 0, stretchToEdge: false)
    let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
    view.showsHorizontalScrollIndicator = false
    view.showsVerticalScrollIndicator = false
    view.backgroundColor = .white
    view.alwaysBounceVertical = false
    view.alwaysBounceHorizontal = true
    self.contentView.addSubview(view)
    return view
  }()

  override func layoutSubviews() {
    super.layoutSubviews()
    collectionView.frame = contentView.bounds
  }

}
benvigier commented 6 years ago

Quick update on this. I managed to achieve the desired result by creating my own CollectionViewFlowLayout for the nested Collection View. I would still love to know whether the same result can be achieved using the (IG)ListCollectionViewLayout. If not, maybe a future version of the framework could allow to specify the desired cell layout at the section controller level.

rnystrom commented 6 years ago

@benvigier could you provide a sample project so we could play around with the error in stacking cells? I wont have a lot of time to dig into this.

benvigier commented 6 years ago

@rnystrom Thanks for the quick response. I just put a quick project together that illustrates the issue. https://github.com/benvigier/UITests

Since I am finally experiencing a bunch of issues with the custom layout I wrote over the weekend, I would really love a solution that works with ListCollectionViewLayout. Thanks again for your help!