Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CollectionLab

Can we build a decent UICollectionView wrapper from SwiftUI, using the iOS 18 custom-container API? This lab's answer: yes — and it's better than decent, because Subview proxies turn out to be first-class citizens: renderable inside UIHostingConfiguration, addressable through container values and tags, and durable enough that cell @State survives reuse.

Layout

  • CollectionUI/ — the library (6 files, an iOS framework target).
  • Chapters/ — the textbook: ten chapter files, one use case each, prose in the header comment, runnable demo below. The app's table of contents is itself a CollectionView.
  • Tests/ — runtime probes in the wuhu-lab style: real UIWindow, real simulator, assertions about what actually happens.
  • This design stands on prior wuhu-app labs (presence, navigation-state, platform-environment), which are internal and not included here.

Run it

cd CollectionLab
xcodegen generate
open CollectionLab.xcodeproj        # run the CollectionLab scheme
# headless chapter runs: simctl launch … dev.wuhu.collectionlab.CollectionLab -chapter 7
# probes:
xcodebuild -project CollectionLab.xcodeproj -scheme CollectionLab \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' test

The design in five sentences

CollectionView resolves its content with Group(sections:) and hands the SectionCollection to a representable. A diffable data source is keyed by interned Int surrogates (diffable identifiers must be Sendable; Subview.ID deliberately isn't). Each cell's contentConfiguration is a UIHostingConfiguration hosting the item's Subview directly, with the live context.environment injected at the root. Sections describe their layout through container values (.collectionLayout(.grid(3)) on a Section), and items describe their sizes the same way — the tag mechanism, used as a sizing channel. A compositional section provider (or your own UICollectionViewLayout, fed through collectionMetrics) turns those declarations into UIKit layout, preferring absolute dimensions whenever the declarations make sizes deterministic.

The three original questions

  1. Tags for preferred width/height — yes. .collectionItemHeight(56), .collectionItemWidth(240), .collectionItemAspectRatio(3/4) are container values; they cascade from Section to items (probe-verified), and any deterministic size becomes an absolute dimension — the sizing negotiation never runs. Chapter 4; waterfall makes it structural in chapter 7.
  2. A "size class" that measures once — yes. .collectionSizingClass("card") measures the first member per (class, width, sizing environment) via an off-window pass over the same hosting configuration real cells use, then applies the exact size to every member. Dynamic Type changes invalidate automatically; 40 cards = 1 measurement (test-pinned). Chapter 5.
  3. Complete custom layout — yes, two tiers. .compositional { context in … } hands you the raw NSCollectionLayoutSection with a hint-aware context (chapter 8); CollectionView(layout: DeckLayout.init) accepts any UICollectionViewLayout, which reads the SwiftUI-declared metrics through collectionView.collectionMetrics (chapter 9).

Findings register

Empirical, on Xcode 26.4 / iOS 26.4 simulator / macOS 26.5 — observed behavior, not contracts:

# Finding Where proven
1 .containerValue on a Section lands on SectionConfiguration.containerValues and cascades to content + header subviews; closest declaration wins macOS render probe
2 Loose content ahead of sections forms an implicit section macOS render probe
3 .tag() reads back via containerValues.tag(for:) / hasTag macOS probe + testScrollToTagReachesItem
4 A value on ForEach stamps every generated subview, without leaking to the section macOS render probe
5 Subview renders correctly inside a cell's UIHostingConfiguration every runtime test
6 Reuse bounds live views: 2,000 rows ⇒ <60 live cells testCellsMaterializeAndReuseBoundsLiveViews
7 Injected environment reaches cell content (custom key set outside the collection observed inside cells) testEnvironmentReachesCells
8 Sizing class: 30 varied cards ⇒ exactly 1 measurement, pixel-identical heights testSizingClassMeasuresOnceAndUnifiesHeights
9 Snapshot updates track SwiftUI state (8→20→3 items) testSnapshotUpdateInsertsAndRemoves
10 onScrolledNearEnd fires once per content version testNearEndFiresOncePerContentVersion
11 @State inside a cell survives scroll-away + return — storage travels with the retained proxy across hosting-tree teardown testStateAcrossReuseVerdict (“SURVIVED”)
12 Diffable identifiers must be Sendable; Subview.ID/SectionConfiguration.ID aren't ⇒ interning table SDK swiftinterface

Honest limitations

  • Finding 11 (and proxy durability generally) is platform behavior, per the presence lab's caveat — regression-test it, don't lean on it for critical state.
  • Orthogonal (carousel) inner scroll offsets aren't addressable from outside; scrollTo(tag:) reaches the section, not the card within.
  • Sizing-class caches don't watch the representative's content — that's the contract; proxy.invalidateSizingClasses() exists for redesigns.
  • Headers/footers are dequeued as UICollectionViewCell supplementaries (cells have contentConfiguration; plain reusable views don't).
  • Selection, swipe actions, drag reorder: out of scope here; cells host fully interactive SwiftUI, which covers most of what those are used for.

Chapters

  1. Your first CollectionView — reuse, self-sizing, pagination for free.
  2. Sections, headers, identity — pinned headers, diffable animations.
  3. Grids and flow — per-section layout dialects in one scroll.
  4. Size hints — the tag-cascade sizing channel; zero-measure cells.
  5. Sizing classes — measure once, share everywhere; Dynamic Type.
  6. Carousels — orthogonal sections; scrollTo(tag:).
  7. Waterfall — masonry as arithmetic over declared ratios.
  8. The escape hatch — raw NSCollectionLayoutSection, hint-aware.
  9. Bring your own layout — UICollectionViewLayout + collectionMetrics.
  10. Lifecycle & environment — the probe bench; what lives and dies in cells.
  11. The update tax — every content update walks all N (rebuildEntries is O(N), like the List machinery it mirrors); measure per-update cost against N, and see per-cell Text(style: .timer) tick for free because time routed inside the render server never becomes a content update.

About

A UICollectionView wrapper built from SwiftUI's iOS 18 custom-container API — chapters, probes, and a findings register

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages