LazyLayoutKit: lazy custom layouts for SwiftUI

SwiftUI offers arbitrary geometry through Layout, and laziness through LazyVStack / LazyVGrid, but not both at once. Layout is eager by construction: sizeThatFits(proposal:subviews:cache:) hands you LayoutSubview values you have to measure, and a view you haven't built can't be measured. Asked about lazy custom layouts at the WWDC26 SwiftUI Group Lab, Apple's answer was that the protocol is eager-only and to file a Feedback.

LazyLayoutKit takes the narrower path: require the caller to supply each item's layout input as data, before any view exists.

public protocol LazyLayoutAlgorithm: Equatable, Sendable {
    associatedtype Item: Equatable & Sendable
    func layout(items: [Item], containerWidth: Double) -> LazyLayoutResult
}

Item is an associated type deliberately: masonry wants an aspect ratio, a timeline wants an interval, a calendar wants a date range. Fixing it to a height would have made this a masonry library with a general-sounding name.

Two implementation notes that might interest people here:

Visibility is answered by a uniform bucket index in compressed-sparse-row form, built over whatever frames the layout emitted. It assumes nothing about structure, so frames may overlap, arrive unordered or use negative coordinates. Items spanning many buckets go on a separate oversized list rather than being duplicated into each, keeping worst-case memory linear. On device the query measures ~2ยตs and stays flat from 100k to 1M items.

Scroll anchoring keys on caller-supplied identity rather than index, because inserting at the front shifts every index and index-based anchoring would silently hold a different item. Notably, the lookup is a sequential scan, not a hash table: building a 100k-entry table costs ~1.4ms on an M4 but ~29ms on an A16, and anchoring needs exactly one lookup per snapshot.

Measured on an iPhone 14 Pro, Release: 120fps at both 100k and 1M items, 8.34ms p99, zero frames over 16.7ms, ~54 cells materialized at any depth. LazyVStack over the same 100k scored the same โ€” one run each over different content, so read it as generality costing little rather than a performance claim.

Scope is deliberately narrow for 0.1: vertical scrolling only, sizes known up front, collection changes correct but unanimated, exact eager snapshots. All stated in the README.

Feedback, contributions and ideas very welcome!

7 Likes

[UPDATE] 0.2 is out - self-sizing text in a lazy SwiftUI container, without a measure-and-correct pass

Read more in CHANGELOG.md

0.3 is out. The part worth writing up rather than changelogging:

Scrolling to an item that has no view. ScrollPosition targets a view by identity, and in a virtualized container the item you are naming usually has not been built, so the request quietly does nothing.

The way out was to resolve the id through the layout instead of the view tree. The container already holds a solved snapshot of every frame, so the target's geometry is known before any view exists and the offset is a subtraction.

My question now:

Is there a supported way to drive a ScrollView to an unmaterialized item that I have missed? I could not find one that does not amount to computing the offset yourself, but I would rather be wrong than ship a parallel mechanism.

Also new: JustifiedLayout, the Photos/Flickr row arrangement. No new device figures this release, the index and query path are unchanged, so 0.2's numbers stand.