After much struggle, I figured out a way to do this. By itself, it's not too hacky, but requires additional hacks to work around what appears to be a UIKit bug.
Caveats
This only works if the height of both your section content AND section headers are absolute and known ahead of time (at least in the section layout provider).
This worked in my case because both my headers and cells are fixed height, and I was only using one row of horizontally-scrolling cells per section.
The background view must not scroll horizontally (assuming a vertically-scrolling collection view.
In my case I wanted the section background images to be fixed, with the cells scrolling horizontally (orthogonally) over top of them.
Constraints/Assumptions
We can only use compositional layout (in my case to get easy orthogonal scrolling).
The facility for setting a background view (mentioned in the question) won't work because it cannot be
configured per-section via a supplementary view registration or any other means.
So we must use a regular supplementary item.
The supplementary item must be on the section to get the right size, which
means only using NSCollectionLayoutBoundarySupplementaryItem.
3.1. While it is possible to trick a different type (e.g. a non-boundary supplementary item on the group)
into being the right size and position, using tricks results in UIKit not
knowing when the background is actually visible, which results in flickering
in/out during scrolling.
Despite being attached to the section, a section supplementary item will be
positioned vertically relative to the cells only, ignoring the header part
of the section.
Worse, this alignment combined with setting the height equal to total section
height results in the supplementary item stretching the height of the section
below the cells, making the section too tall.
There is a facility for setting a position offset for the supplementary item,
however due to what I can only assume is a bug (even with an offset of 0),
using it causes the section header height to collapse to zero, resulting in the
cells below moving up and and underlapping it.
Solution
So, to work around all of that, what I did is:
- Set the background supplementary item size to be the known height of header + cells/row + any spacing between them.
- Pull the background item up under the header by offsetting vertically by the
header height (using an offset causes the cells to move up; see constraint 6).
- Push the cells back down by padding the section insets top by the height of
the header (
hackInsets
).
- Pull the background's leading/trailing edges out to the view/screen edges, to
make it full-bleed (normally it would be inset like the content). We do this by
setting negative margins on the background image view in the background
supplementary view registration.
- Set a negative z-index on the background view to keep it under the cells and header.
In code, the relevant parts look like this:
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(Self.headerHeight))
let headerSupplementaryItem = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: ElementKind.header, alignment: .top)
let hackInsets = NSDirectionalEdgeInsets(top: Self.headerHeight, leading: 0, bottom: 0, trailing: 0)
let backgroundHeight = MySectionHeaderView.nominalSize.height + Self.interItemSpacing + MyCollectionViewCell.nominalSize.height
let backgroundOffset = CGPoint(x: 0, y: -MySectionHeaderView.nominalSize.height)
let backgroundAnchor = NSCollectionLayoutAnchor(edges: .top, absoluteOffset: backgroundOffset)
let backgroundSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(backgroundHeight))
let backgroundSupplementaryItem = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: backgroundSize, elementKind: ElementKind.background, containerAnchor: backgroundAnchor)
backgroundSupplementaryItem.zIndex = -1
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
section.contentInsets = Self.sectionInsets.summedWith(hackInsets)
section.boundarySupplementaryItems = [headerSupplementaryItem, backgroundSupplementaryItem]