Skip to content
macOSDeep Dive Published Updated 7 min readViews unavailable

NSCollectionView Compositional Layout on macOS: Sections, Sizing, and Adaptation

Compose adaptive AppKit collection layouts from items, groups, and sections while keeping sizing, supplementary content, and model identity predictable.

NSCollectionViewCompositionalLayout builds a collection layout from nested components: items describe the smallest displayed unit, groups arrange items, and sections organize groups. That hierarchy makes different regions of one collection easier to express without one enormous layout subclass. It does not remove the need to reason about item identity, self-sizing content, container width, scrolling, supplementary views, or the cost of invalidating a layout as the window changes.

The design goal is not merely to create a visually flexible grid. It is to make the layout function predictable for each section and environment, then verify that its size rules remain coherent when content, window width, display scale, localization, and user text size change. Collection view data and layout geometry are separate systems; neither one should guess the other’s state.

Build upward from item to layout

An item has a layout size, a group contains one or more items, and a section contains groups. A section provider can return a different section layout for each section index and receives a layout environment. That makes section-specific and width-sensitive layouts explicit. Keep the provider deterministic: the same section and environment should produce the same layout structure rather than depending on transient cell state.

import AppKit

func makeAdaptiveLayout() -> NSCollectionViewCompositionalLayout {
    NSCollectionViewCompositionalLayout { sectionIndex, environment in
        let width = environment.container.effectiveContentSize.width
        let columnCount = width >= 720 ? 3 : 1
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1.0),
            heightDimension: .estimated(180)
        )
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        let groupSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1.0),
            heightDimension: .estimated(180)
        )
        let group = NSCollectionLayoutGroup.horizontal(
            layoutSize: groupSize,
            subitem: item,
            count: columnCount
        )
        let section = NSCollectionLayoutSection(group: group)
        section.interGroupSpacing = sectionIndex == 0 ? 12 : 8
        return section
    }
}

The example uses an illustrative threshold and estimated height, not a universal design breakpoint. Measure the available content width rather than the screen width because sidebars, split views, and window borders affect the collection’s actual container. If a layout section depends on a specific item count or supplementary configuration, use the section index to express that schema and test it when sections are inserted or removed.

Fractional, absolute, and estimated dimensions

Fractional dimensions are relative to the containing layout item, group, or section context. Absolute dimensions are useful for fixed controls or known spacing but can clip localized text or fail under accessibility sizing. Estimated dimensions let the layout begin with an estimate and refine geometry based on content where the item supports self-sizing. An estimate is not an exact constraint and should not be used to hide a cell whose Auto Layout constraints are ambiguous.

Use a sizing model appropriate to the content. A photo tile can have a fixed aspect ratio if the image is cropped consistently. A variable-height text card may use estimated height with complete internal constraints. A grid can use fractional widths and a known aspect ratio. Avoid making a group height depend on an unrelated absolute value if cell content can grow, because the group can then clip or leave inconsistent gaps.

Insets, inter-item spacing, inter-group spacing, and section content insets are distinct. Decide which component owns each gap and avoid applying the same margin both to a cell’s internal constraints and the section’s outer insets. For horizontal groups, item spacing can be expressed through the group’s inter-item spacing or item edge spacing depending on the desired distribution. Test the actual resulting geometry, especially where items use fractional widths and spacing consumes container width.

Adapt to the collection’s environment

The section provider receives NSCollectionLayoutEnvironment; its container describes the available size and traits relevant to layout. Use it to select a composition that matches the current width, such as one column in a narrow pane and multiple columns in a wide window. Avoid using the current display’s pixel dimensions as a substitute for the collection container. A resizable Mac window can become narrower than an iPad-like breakpoint while on a large external display.

Do not overfit a layout to a single numeric threshold. If a three-column grid at 720 points makes each card too narrow after accounting for section insets and spacing, the breakpoint is wrong for the content. Calculate usable width: subtract content insets and all inter-item gaps, then divide by column count. Set a minimum readable card width based on actual text and controls, not aesthetic preference alone.

Window resize can trigger layout recomputation frequently. Keep the section provider lightweight and avoid fetching models, decoding images, or mutating data sources from it. Precompute cheap layout parameters from the environment. If a transition changes semantic section structure, coordinate the update with a data-source snapshot rather than changing collection content from inside a geometry callback.

Supplementary and decoration views

Headers and footers are part of a section’s layout definition and must have a corresponding element kind and registered or otherwise configured view provider. A layout that declares supplementary content without a data-source path for that element can fail or display missing content. Treat the element kind string as a stable interface between layout and registration code, and centralize its definition to avoid subtle spelling mismatches.

Decoration views are layout-owned visual elements, not ordinary data items. Use them for backgrounds or separators that belong to layout geometry rather than inserting fake model rows. Keep accessibility semantics aligned with that choice; a purely decorative background generally should not become a confusing extra accessibility stop. For section backgrounds, validate z-index and supplementary stacking when items overlap or scroll.

Orthogonal scrolling can create horizontally scrolling content within a vertically scrolling collection. It is useful for a carousel-like section but introduces independent scroll state, gesture competition, and a larger accessibility navigation surface. Test trackpad, mouse wheel, keyboard focus, and VoiceOver navigation. Do not use nested scrolling just to avoid deciding how the entire screen should scroll.

Layout changes do not define data identity

The layout describes geometry; the data source describes which models exist and their identities. With diffable data sources, use stable identifiers that do not change when an item moves between sections or when its title is edited. Apply snapshots on the appropriate UI executor and coordinate data changes with any layout assumptions about item counts or section identifiers.

A cell should be a rendering of its current item, not a long-lived cache of the previous item at the same index path. Reset all state in the cell configuration path: image task, selection state, accessibility label, and async result generation. When an image request completes after reuse, compare its stable item identifier or generation before applying it. A sophisticated layout cannot prevent stale content from appearing in reused cells.

When applying a snapshot changes section structure, do not also mutate the layout provider’s external section array without a consistent update order. A snapshot and provider that disagree about section index can request a layout for the wrong section. Prefer mapping section identifiers to layout types, or maintain one immutable view-state snapshot from which both data and layout are derived.

Debug sizing and invalidation

When items overlap, collapse, or appear with unexpected spacing, inspect the component hierarchy and the values of the relevant layout dimensions. Compare expected container width with effective content size, then account for insets, spacing, and fractional dimensions. Verify cell Auto Layout constraints independently: collection geometry can be correct while a cell’s internal constraints are ambiguous or conflicting.

Avoid forcing a full layout invalidation after every model update. Let the collection view’s data source and layout APIs update what changed; use explicit invalidation when geometry inputs change, such as a width class or dynamic content size. Measure large collections under resize and scrolling. Recomputing complex custom groups on every event can make the interface janky even when cell rendering is cheap.

Validation matrix

Test an empty collection, one item, counts below and above a row boundary, long localized titles, missing images, dynamic text, narrow split-view width, wide window, external display, live resize, inserted and deleted sections, and supplementary views. Assert that item identifiers map to the intended cells, no content clips at supported sizes, section headers remain associated with the right section, and accessibility navigation follows visual order.

For performance, measure initial layout, scroll hitching, resize cost, snapshot application, and memory from prefetched or cached cell content. Test with production-scale counts rather than ten sample items. Record the container sizes and device configuration used by the test because a breakpoint validated on one window width is not a cross-device guarantee.

Compositional layout is most maintainable when item/group/section sizing has one clear owner and each section’s layout is a pure function of stable model identity plus current environment. It gives AppKit a declarative geometry tree; the application remains responsible for content fitting, interaction, data identity, and accessibility.

Related:

Sources:

Comments