I have a custom header/footer class
class CustomCollectionReusableView: UICollectionReusableView {
var pageControl: UIPageControl!
...
}
and use it in UICollectionViewDataSource
appropriate method:
func collectionView(
_ collectionView: UICollectionView,
viewForSupplementaryElementOfKind kind: String,
at indexPath: IndexPath
) -> UICollectionReusableView {
if kind == customKind {
...//return CustomCollectionReusableView object
}
...
}
The problem is if I have multiple sections with the same this header/footer then how should I get callback to know in which of them user clicked on pageControl
(get indexPath)?
If it was UITableViewCell
/UICollectionViewCell
subclass I could just call a custom delegate in it with a cell-sender param and then call for example collectionView.indexPath(for:)
.
The problem is there is no such method for headers/footers. There is collectionView.indexPathsForVisibleSupplementaryElements(ofKind:)
only which returns multiple indexPath
objects.
How to resolve this issue correctly? Should I set callback block or indexPath inside this view?
You can use protocol/delegate pattern, but Swift likes closures.
Depending on what all you need to do, you may want to take different approaches.
IF you know your collection view data structure will not change, you can capture the section
in a closure.
For example:
class InteractiveHeaderView: UICollectionReusableView {
public var myValueChanged: ((Int) -> ())?
@objc func pgChanged(_ sender: UIPageControl) {
myValueChanged?(sender.currentPage)
}
// all the rest of your header (supplementary) view setup
}
Then, your viewForSupplementaryElementOfKind
might look something like this:
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionView.elementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: InteractiveHeaderView.identifier, for: indexPath) as! InteractiveHeaderView
let thisSection: Int = indexPath.section
// set properties in headerView
// set the closure
headerView.myValueChanged = { [weak self] val in
guard let self = self else { return }
// do something based on the val passed by the section's header view
self.updateValueForSection(section: thisSection, val: val)
}
return headerView
default:
assert(false, "Invalid element type")
}
}
On the other hand, suppose you have 30 sections, and based on user-interaction or data events, you might be deleting / inserting / re-ordering sections, you don't want to rely on the captured section
.
In that case, we can pass back the header view instance, and loop through the visible supplementary views to find the index path.
So our closure
changes to:
class InteractiveHeaderView: UICollectionReusableView {
public var myValueChanged: ((UICollectionReusableView, Int) -> ())?
@objc func pgChanged(_ sender: UIPageControl) {
myValueChanged?(self, sender.currentPage)
}
// all the rest of your header (supplementary) view setup
}
and we can do this when setting the closure:
headerView.myValueChanged = { [weak self] thisView, val in
guard let self = self else { return }
// user interacted with a section header view, so
// the header view executing this closure must be visible...
// get the indexPaths of the visible supplementary views
let idxPaths = self.collectionView.indexPathsForVisibleSupplementaryElements(ofKind: kind)
for pth in idxPaths {
if let thisHeaderView = self.collectionView.supplementaryView(forElementKind: kind, at: pth), thisHeaderView == thisView {
// do something based on the val passed by the section's header view
self.updateValueForSection(section: pth.section, val: val)
}
}
}
Here's a complete example..
We'll start by defining a "section" data struct:
struct MySection {
var myVal: Int = 0
var numItems: Int = 0
}
Where myVal
will be an index into an array of colors, which we'll use for the background of the label in a collection view cell, and numItems
will be the number of items in the section.
It will look like this when running:
Changing the current page in the section header will change the background color of the labels in that section:
SimpleCell: UICollectionViewCell
class - with a single, centered label...
class SimpleCell: UICollectionViewCell {
static let identifier: String = "simpleCell"
public var bkgColor: UIColor = .systemRed { didSet { theLabel.backgroundColor = bkgColor } }
public var theLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12, weight: .bold)
label.font = .monospacedSystemFont(ofSize: 12, weight: .bold)
label.textColor = .white
label.textAlignment = .center
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
theLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(theLabel)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
theLabel.topAnchor.constraint(equalTo: g.topAnchor),
theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
theLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
theLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
theLabel.backgroundColor = bkgColor
// so we can see the cell framing
contentView.backgroundColor = .yellow
contentView.layer.borderColor = UIColor.black.cgColor
contentView.layer.borderWidth = 1
}
}
InteractiveHeaderView: UICollectionReusableView
class - with a label and a page control...
class InteractiveHeaderView: UICollectionReusableView {
static let identifier: String = "interactiveHeaderView"
public var myValueChanged: ((UICollectionReusableView, Int) -> ())?
public var title: String = "" { didSet { titleLabel.text = title } }
public var numPages: Int = 1 { didSet { pageControl.numberOfPages = numPages } }
public var curPage: Int = 0 { didSet { pageControl.currentPage = curPage } }
private let titleLabel: UILabel = UILabel()
private let pageControl: UIPageControl = UIPageControl()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
[titleLabel, pageControl].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
}
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
pageControl.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
pageControl.centerYAnchor.constraint(equalTo: centerYAnchor),
])
backgroundColor = UIColor(red: 0.0, green: 0.2, blue: 0.5, alpha: 1.0)
titleLabel.textColor = .white
pageControl.backgroundStyle = .prominent
pageControl.numberOfPages = numPages
pageControl.addTarget(self, action: #selector(pgChanged(_:)), for: .valueChanged)
}
@objc func pgChanged(_ sender: UIPageControl) {
myValueChanged?(self, sender.currentPage)
}
}
InteractiveHeaderViewController: UIViewController
class - to demonstrate...
class InteractiveHeaderViewController: UIViewController {
var collectionView: UICollectionView!
var myData: [MySection] = []
let sectionColors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue,
.systemBrown, .systemCyan
]
override func viewDidLoad() {
super.viewDidLoad()
// some sample data
let iCounts: [Int] = [
7, 16, 9, 5, 8, 12, 13, 10, 3, 7, 11
]
iCounts.forEach { n in
// we'll start the data with all sections using color Zero
let s = MySection(myVal: 0, numItems: n)
myData.append(s)
}
let fl = UICollectionViewFlowLayout()
fl.scrollDirection = .vertical
fl.minimumLineSpacing = 4.0
fl.minimumInteritemSpacing = 4.0
fl.itemSize = .init(width: 54.0, height: 36.0)
fl.sectionInset = .init(top: 4.0, left: 4.0, bottom: 4.0, right: 4.0)
fl.headerReferenceSize = .init(width: 200.0, height: 60.0)
collectionView = UICollectionView(frame: .zero, collectionViewLayout: fl)
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 2.0),
collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
collectionView.register(SimpleCell.self, forCellWithReuseIdentifier: SimpleCell.identifier)
collectionView.register(InteractiveHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: InteractiveHeaderView.identifier)
collectionView.dataSource = self
collectionView.delegate = self
collectionView.backgroundColor = .systemYellow
}
func updateValueForSection(section: Int, val: Int) {
myData[section].myVal = val
for i in 0..<collectionView.numberOfItems(inSection: section) {
if let c = collectionView.cellForItem(at: IndexPath(item: i, section: section)) as? SimpleCell {
c.bkgColor = sectionColors[myData[section].myVal]
}
}
}
}
extension InteractiveHeaderViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UICollectionViewDelegate {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return myData.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return myData[section].numItems
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SimpleCell.identifier, for: indexPath) as! SimpleCell
cell.theLabel.text = "\(indexPath.section),\(indexPath.item)"
cell.bkgColor = sectionColors[myData[indexPath.section].myVal]
return cell
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionView.elementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: InteractiveHeaderView.identifier, for: indexPath) as! InteractiveHeaderView
let thisSection: Int = indexPath.section
headerView.title = "Sec: \(thisSection)"
headerView.numPages = sectionColors.count
headerView.curPage = myData[thisSection].myVal
headerView.myValueChanged = { [weak self] thisView, val in
guard let self = self else { return }
// user interacted with a section header view, so
// the header view executing this closure must be visible...
// get the indexPaths of the visible supplementary views
let idxPaths = self.collectionView.indexPathsForVisibleSupplementaryElements(ofKind: kind)
for pth in idxPaths {
if let thisHeaderView = self.collectionView.supplementaryView(forElementKind: kind, at: pth), thisHeaderView == thisView {
// do something based on the val passed by the section's header view
self.updateValueForSection(section: pth.section, val: val)
}
}
}
return headerView
default:
assert(false, "Invalid element type")
}
}
}