I am trying to create a ListBindingSectionController with sticky headers. I followed the ModelingAndBinding example and was able to get my list working with the headers as a cell, represented by their own DataModel, however I would like to take advantage of sticky headers per section. I looked at the example code for SupplementaryViewController, with the FeedItemSectionController, to see how to use sticky headers. However the FeedItemSectionController is a SectionController and has access to the top level data object, whereas as far as I can tell ListBindingSectionControllers do not. Additionally data binding with the header view also doesn't seem to work. I am able to get the sticky header to appear above each section, and it is sticking when I pass it junk text, but I can't figure out how to pass the header data. What is the recommended way to get the high level per section data to my supplementary header?
Header View:
import UIKit
import IGListKit
// ListBindable doesn't appear to do anything now that I am using this as a header not a cell
class Header: UICollectionViewCell, ListBindable {
let label: UILabel = {
let label = UILabel()
label.textColor = UIColor.black
return label
}()
// This was working when I used this view as a cell, but as a header, this isn't called
func bindViewModel(_ viewModel: Any) {
guard let viewModel = viewModel as?HeaderViewModel else { return }
label.text = viewModel.title
}
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = UIColor.white
contentView.addSubview(label)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
let padding = UIEdgeInsets(top: 8, left: 15, bottom: 8, right: 15)
label.frame = UIEdgeInsetsInsetRect(bounds, UIEdgeInsetsMake(0, padding.left, 0, padding.right))
}
}
SectionController:
import UIKit
import IGListKit
class MySectionController: ListBindingSectionController<MyData>, ListBindingSectionControllerDataSource {
var data: MyData! // This was being set when I used a plain SectionController, but is no longer being set with the ListBindingSectionController
override init() {
super.init()
dataSource = self
supplementaryViewSource = self
inset = UIEdgeInsets(top: 0, left: 0, bottom: 15, right: 0)
}
// MARK: ListBindingSectionControllerDataSource
func sectionController(
_ sectionController: ListBindingSectionController<ListDiffable>,
viewModelsFor object: Any
) -> [ListDiffable] {
guard let object = object as? MyData else { fatalError() }
let header: [ListDiffable] = [
HeaderViewModel(title: data.title)
]
return object.images // I can return header + object.images here, and treat the header as a cell, but I want to use sticky headers.
}
func sectionController(
_ sectionController: ListBindingSectionController<ListDiffable>,
sizeForViewModel viewModel: Any,
at index: Int
) -> CGSize {
guard let width = collectionContext?.containerSize.width else { fatalError() }
let height: CGFloat
switch viewModel {
case is HeaderViewModel: height = 50 // This is never true since I am not passing the header as a cell anymore, left it in to demo what was working previously
case is ImageViewModel: height = 70
default: height = 0
}
return CGSize(width: width, height: height)
}
func sectionController(
_ sectionController: ListBindingSectionController<ListDiffable>,
cellForViewModel viewModel: Any,
at index: Int
) -> UICollectionViewCell {
let cellClass: AnyClass = viewModel is HeaderViewModel ? Header.self : ImageCell.self
return collectionContext!.dequeueReusableCell(of: cellClass, for: self, at: index)
}
}
// MARK: Header methods
extension DailyLooksByMonthSectionController : ListSupplementaryViewSource {
func supportedElementKinds() -> [String] {
return [UICollectionElementKindSectionHeader]
}
func viewForSupplementaryElement(ofKind elementKind: String, at index: Int) -> UICollectionReusableView {
guard let view = collectionContext?.dequeueReusableSupplementaryView(ofKind: elementKind, for: self, class: Header.self, at: index) as? Header else {
fatalError()
}
// Would like to set view.label.text = data.title here, but dont have access to data
// note: setting view.label.text = "Junk" makes sticky headers work fine
return view
}
func sizeForSupplementaryView(ofKind elementKind: String, at index: Int) -> CGSize {
return CGSize(width: collectionContext!.containerSize.width, height: 50)
}
}
README and documentationIGListKit version: Latest# Please include debug logs using the following lldb command:
po [IGListDebugger dump]
If you are using ListBindingSectionController you will have a property named object which will have the view model you are binding. Together with the viewModels-property you should be able to get the appropriate data to set the cell. The ListBindingSectionController and ListGenericSectionController both are automatically calling didUpdateToObject which sets the object-property (basically your data in your case).
Regarding the sticky headers is driven by the UICollectionViewLayout you are using. You can use the Instagram's one but then you need to initialise it accordingly so that you enable stickiness and set the appropriate top offset. Alternatively, if your iOS minimum version allows it you can also consider using the iOS's flow layout's property sectionHeadersPinToVisibleBounds see the documentation:
When this property is true, section header views scroll with content until they reach the top of the
screen, at which point they are pinned to the upper bounds of the collection view. Each new header
view that scrolls to the top of the screen pushes the previously pinned header view offscreen.
@vicki15 IGListBindingSectionController is still just a section controller. You can attach a supplementaryViewSource just like you're doing.
Is your question mostly around _updating_ the header view after something changes? If so, that's definitely a limitation. I'm tempted to bump min OS support to iOS 9 so that we can take advantage of some of UICollectionView's supplementary querying APIs...
Awesome thank you! The object property is what I was looking for. I was confused because in the regular section controller, I accessed the data by having a variable
data: Data!
And setting it in the didUpdate function. Thank you for the help! Fortunately I don't have to update the header, other than removing it along with the section when there is no data in that section. Although now that I mention it, I might not get that for free since the top level object wont be empty - it will contain an empty array. But I'll look into that. Thanks again! I'll close this issue out.
@rnystrom Any update on this? I am not able update the header using self.update(animated: true, completion: nil).
Unfortunately I鈥檓 not actively maintaining this project at the time. The project is still maintained by the wonderful folks at Instagram, and they would gladly review a PR to fix this!
Most helpful comment
If you are using
ListBindingSectionControlleryou will have a property namedobjectwhich will have the view model you are binding. Together with theviewModels-property you should be able to get the appropriate data to set the cell. TheListBindingSectionControllerandListGenericSectionControllerboth are automatically callingdidUpdateToObjectwhich sets theobject-property (basically yourdatain your case).Regarding the sticky headers is driven by the UICollectionViewLayout you are using. You can use the Instagram's one but then you need to initialise it accordingly so that you enable stickiness and set the appropriate top offset. Alternatively, if your iOS minimum version allows it you can also consider using the iOS's flow layout's property
sectionHeadersPinToVisibleBoundssee the documentation: