Use a custom flow layout to perfectly manage the height of a header in a collection view with the Dynamic Type feature.
A header element is seen as a supplementary element for a collection view and the referenceSizeForHeaderInSection 'method' is only used for initialization: it's not called with the Dynamic Type feature.
The solution hereafter is based on the layoutAttributesForElements method of the custom layout that will be able to adapt the header height thanks to the UIFontMetrics scaledValue.
All that is fired by the invalidateLayout method called in the traitCollectionDidChange triggered when the user changes the font size.
STEP 1 ⟹ create a simple custom header class as follows for instance:
class MyHeaderClass: UICollectionReusableView {
override init(frame: CGRect) { super.init(frame: frame) }
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) }
}
STEP 2 ⟹ create a new empty .xib adding a reusable view and name it the exact same name as the class it refers to: don't forget to change its class name in the Identity Inspector.
STEP 3 ⟹ register the .xib file in the controller:
collectionView.register(UINib(nibName: collectionViewHeaderFooterReuseIdentifier bundle: nil),
forSupplementaryViewOfKind: UICollectionElementKindSectionHeader,
withReuseIdentifier:collectionViewHeaderFooterReuseIdentifier)
STEP 4 ⟹ support this new cell in your data source (a header is a supplementary element for a collection view):
func collectionView(_ collectionView: UICollectionView,
viewForSupplementaryElementOfKind kind: String,
at indexPath: IndexPath) -> UICollectionReusableView {
if (kind == UICollectionView.elementKindSectionHeader) {
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind,
withReuseIdentifier: collectionViewHeaderReuseIdentifier,
for: indexPath) as! MyHeader
headerView.myLabel.text = "Your Header Title"
return headerView
} else {
return UICollectionReusableView(frame: CGRect.null) }
}
... and in your delegate (this intializes the header size and makes it appear):
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: collectionView.frame.width, height: headerHeight)
}
.. after adding a global var headerHeight: CGFloat = 90.0 for initialization.
STEP 5 ⟹ create the custom flow layout to adapt the header height to the new font size:
class FlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)
layoutAttributes?.forEach({ (attribute) in
if (attribute.representedElementKind == UICollectionView.elementKindSectionHeader) {
headerHeight = UIFontMetrics.default.scaledValue(for: 22.0)
attribute.frame.size.height = headerHeight
}
})
return layoutAttributes
}
}
Don't forget to update the storyboard in Interface Builder:
STEP 6 ⟹ inform the controller to trigger a layout update when the user changes the font size:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
if (previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory) {
collectionView?.collectionViewLayout.invalidateLayout()
}
}
Following this rationale, the correct height of the headers is automatically set up according to the headers font size ⟹ I suggest to use the Xcode 11 new feature to test the Dynamic Type very quickly.