In my previous article, we had discussed how to add a declarative header and footer to the UICollectionView
List. This week, let’s take one step further and talk about how you can add a custom header to your collection view declaratively.
This is a continuation of my previous article “Declarative UICollectionView List Header and Footer“, therefore do make sure to check it out before proceeding.
The Sample App
To keep things as simple as possible, we will create a simple custom header that consists of a title label and an info button. When tap on the info button, the app will display an alert showing detailed information about that particular section.
Creating the Custom Header
First things first, let’s create the custom header by subclassing the UICollectionReusableView
.
class InteractiveHeader: UICollectionReusableView {
let titleLabel = UILabel()
let infoButton = UIButton()
// Callback closure to handle info button tap
var infoButtonDidTappedCallback: (() -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
fatalError("Not implemented")
}
}
Do note that the init(frame:)
method is compulsory when it comes to creating a declarative custom header. Without it, the collection view will not be able to dequeue it as a reusable supplementary view.
Also note that we have defined the infoButtonDidTappedCallback
closure to handle the info button tap event. We will get into that in a bit.
The configure()
method that we call within the init(frame:)
method will take care of laying out all the custom header UI elements. Here’s the implementation:
extension InteractiveHeader {
func configure() {
// Add a stack view to section container
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .fill
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
stackView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
stackView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor),
])
// Setup label and add to stack view
titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
stackView.addArrangedSubview(titleLabel)
// Set button image
let largeConfig = UIImage.SymbolConfiguration(scale: .large)
let infoImage = UIImage(systemName: "info.circle.fill", withConfiguration: largeConfig)?.withTintColor(.systemPink, renderingMode: .alwaysOriginal)
infoButton.setImage(infoImage, for: .normal)
// Set button action
infoButton.addAction(UIAction(handler: { [unowned self] (_) in
// Trigger callback when button tapped
self.infoButtonDidTappedCallback?()
}), for: .touchUpInside)
// Add button to stack view
stackView.addArrangedSubview(infoButton)
}
}
As can be seen from the above code, we are using a UIStackView
to layout both the label and the button. The following diagram illustrates how the auto layout constraints are being configured from within the custom header.
Also, note that we are using the UIButton
closure-based API (introduced in iOS 14) to set up the infoButton
‘s touchUpInside
event. When the button is being tapped, we will trigger the infoButtonDidTappedCallback
to send the tap event back to the view controller.
Displaying the Custom Header
In order to display the custom header, we must first set the collection view layout header mode to .supplementary
.
var layoutConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
layoutConfig.headerMode = .supplementary
let listLayout = UICollectionViewCompositionalLayout.list(using: layoutConfig)
After that, create a supplementary registration object using the InteractiveHeader
class and make sure to set its element kind to UICollectionView.elementKindSectionHeader
.
let headerRegistration = UICollectionView.SupplementaryRegistration
<InteractiveHeader>(elementKind: UICollectionView.elementKindSectionHeader) {
[unowned self] (headerView, elementKind, indexPath) in
// 1
// Obtain header item using index path
let headerItem = self.dataSource.snapshot().sectionIdentifiers[indexPath.section]
// 2
headerView.titleLabel.text = headerItem.title
// 3
headerView.infoButtonDidTappedCallback = { [unowned self] in
// Show an alert when user tap on infoButton
let symbolCount = headerItem.symbols.count
let alert = UIAlertController(title: "Info", message: "This section has \(symbolCount) symbols.", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
self.present(alert, animated: true)
}
}
Within the supplementary view handler, we:
- Obtain
headerItem
for that particular section from the collection view’s data source. - Set the section title to the custom header’s
titleLabel
. - Implement the custom header’s
infoButtonDidTappedCallback
. For simplicity’s sake, we will display a simple alert when the info button is tapped.
Lastly, we must define the data source’s supplementary view provider.
dataSource.supplementaryViewProvider = { [unowned self]
(collectionView, elementKind, indexPath) -> UICollectionReusableView? in
// Dequeue header view
return self.collectionView.dequeueConfiguredReusableSupplementary(
using: headerRegistration, for: indexPath)
}
Do note how we dequeue a reusable supplementary view using the headerRegistration
object we created not long ago.
With that, the custom header is now ready to be shown on the collection view. Build and run your sample app to see everything in action.
Adjusting the Custom Header’s Height
At this stage, you should see that our custom header’s height is a little bit too small, causing the title label and info button too close to the section.
To go about this, you must first understand that a collection view header is self-sizing, meaning UIKit will calculate the header view size based on its content.
With that in mind, we can easily adjust the header height by adding a top and bottom padding to the custom header’s stack view.
Now head back to the InteractiveHeader
class and update the configure()
method.
// Adjust top anchor constant & priority
let topAnchor = stackView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor, constant: 15)
topAnchor.priority = UILayoutPriority(999)
// Adjust bottom anchor constant & priority
let bottomAnchor = stackView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor, constant: -10)
bottomAnchor.priority = UILayoutPriority(999)
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
topAnchor,
bottomAnchor,
])
As seen from the above code, we can increase the stack view’s top and bottom padding by adjusting the stack view’s top and bottom anchor constant.
On top of that, we also change the top and bottom anchor priority to 999 in order to avoid the unsatisfiable constraints error. To learn more about this, you can refer to the “Dealing with Cell Height” section of “Designing Custom UICollectionViewListCell in Interface Builder“.
Now run you sample code again to see the header’s height updated to what we want.
Designing with Interface Builder
As mentioned earlier, the init(frame:)
method is compulsory when implementing the custom header class, which also means that the awakeFromNib()
method won’t be called during initialization. Because of that, we will not be able to use the traditional interface builder way to design the custom header.
The workaround for this is actually quite simple. Take our InteractiveHeader
as an example, within its configure()
method, instead of constructing the header’s layout programmatically, just load a custom UIView
that is connected to a Xib file into the header’s content view.
I have covered the core concept of how you can use the interface builder to design a UIView
in my previous article “Designing Custom UICollectionViewListCell in Interface Builder“. Thus I will leave this as an exercise for you!
Hint: The SFSymbolNameContentView
‘s loadNib()
method is a good starting point.
Wrapping Up
Even though this article only covers how to create a custom UICollectionView
list header, you can basically apply the exact same technique to create your own custom footer.
You can find the full sample code of this article on Github.
If you like this article, feel free to follow me on Twitter, and subscribe to my monthly newsletter.
Thanks for reading. 👨🏻💻
Further Readings
- Table and Collection View Cells Reload Improvements in iOS 15
- UICollectionView List with Custom Cell and Custom Configuration
- Building an Expandable List Using UICollectionView: Part 1
- Building an Expandable List Using UICollectionView: Part 2
- Replicate the Expandable Date Picker Using UICollectionView List
👋🏻 Hey!
While you’re still here, why not check out some of my favorite Mac tools on Setapp? They will definitely help improve your day-to-day productivity. Additionally, doing so will also help support my work.
- ✨ Bartender: Superpower your menu bar and take full control over your menu bar items.
- ✨ CleanShot X: The best screen capture app I’ve ever used.
- ✨ PixelSnap: Measure on-screen elements with ease and precision.
- ✨ iStat Menus: Track CPU, GPU, sensors, and more, all in one convenient tool.