This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
417 lines
8.3 KiB
Markdown
417 lines
8.3 KiB
Markdown
---
|
|
name: axiom-swiftui-containers-ref
|
|
description: Reference — SwiftUI stacks, grids, outlines, and scroll enhancements through iOS 26
|
|
license: MIT
|
|
metadata:
|
|
version: "1.2.0"
|
|
---
|
|
|
|
# SwiftUI Containers Reference
|
|
|
|
Stacks, grids, outlines, and scroll enhancements. iOS 14 through iOS 26.
|
|
|
|
**Sources**: WWDC 2020-10031, 2022-10056, 2023-10148, 2024-10144, 2025-256
|
|
|
|
## Quick Decision
|
|
|
|
| Use Case | Container | iOS |
|
|
|----------|-----------|-----|
|
|
| Fixed views vertical/horizontal | VStack / HStack | 13+ |
|
|
| Overlapping views | ZStack | 13+ |
|
|
| Large scrollable list | LazyVStack / LazyHStack | 14+ |
|
|
| Multi-column grid | LazyVGrid | 14+ |
|
|
| Multi-row grid (horizontal) | LazyHGrid | 14+ |
|
|
| Static grid, precise alignment | Grid | 16+ |
|
|
| Hierarchical data (tree) | List with `children:` | 14+ |
|
|
| Custom hierarchies | OutlineGroup | 14+ |
|
|
| Show/hide content | DisclosureGroup | 14+ |
|
|
|
|
---
|
|
|
|
## Part 1: Stacks
|
|
|
|
### VStack, HStack, ZStack
|
|
|
|
```swift
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text("Title")
|
|
Text("Subtitle")
|
|
}
|
|
|
|
HStack(alignment: .top, spacing: 8) {
|
|
Image(systemName: "star")
|
|
Text("Rating")
|
|
}
|
|
|
|
ZStack(alignment: .bottomTrailing) {
|
|
Image("photo")
|
|
Badge()
|
|
}
|
|
```
|
|
|
|
**ZStack alignments**: `.center` (default), `.top`, `.bottom`, `.leading`, `.trailing`, `.topLeading`, `.topTrailing`, `.bottomLeading`, `.bottomTrailing`
|
|
|
|
### Spacer
|
|
|
|
```swift
|
|
HStack {
|
|
Text("Left")
|
|
Spacer()
|
|
Text("Right")
|
|
}
|
|
|
|
Spacer(minLength: 20) // Minimum size
|
|
```
|
|
|
|
---
|
|
|
|
### LazyVStack, LazyHStack (iOS 14+)
|
|
|
|
Render children only when visible. Use inside ScrollView.
|
|
|
|
```swift
|
|
ScrollView {
|
|
LazyVStack(spacing: 0) {
|
|
ForEach(items) { item in
|
|
ItemRow(item: item)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Pinned Section Headers
|
|
|
|
```swift
|
|
ScrollView {
|
|
LazyVStack(pinnedViews: [.sectionHeaders]) {
|
|
ForEach(sections) { section in
|
|
Section(header: SectionHeader(section)) {
|
|
ForEach(section.items) { item in
|
|
ItemRow(item: item)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Part 2: Grids
|
|
|
|
### Grid (iOS 16+)
|
|
|
|
Non-lazy grid with precise alignment. Loads all views at once.
|
|
|
|
```swift
|
|
Grid(alignment: .leading, horizontalSpacing: 10, verticalSpacing: 10) {
|
|
GridRow {
|
|
Text("Name")
|
|
TextField("Enter name", text: $name)
|
|
}
|
|
GridRow {
|
|
Text("Email")
|
|
TextField("Enter email", text: $email)
|
|
}
|
|
}
|
|
```
|
|
|
|
**Modifiers**:
|
|
- `gridCellColumns(_:)` — Span multiple columns
|
|
- `gridColumnAlignment(_:)` — Override column alignment
|
|
|
|
```swift
|
|
Grid {
|
|
GridRow {
|
|
Text("Header").gridCellColumns(2)
|
|
}
|
|
GridRow {
|
|
Text("Left")
|
|
Text("Right").gridColumnAlignment(.trailing)
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### LazyVGrid (iOS 14+)
|
|
|
|
Vertical-scrolling grid. Define **columns**; rows grow unbounded.
|
|
|
|
```swift
|
|
let columns = [
|
|
GridItem(.flexible()),
|
|
GridItem(.flexible()),
|
|
GridItem(.flexible())
|
|
]
|
|
|
|
ScrollView {
|
|
LazyVGrid(columns: columns, spacing: 16) {
|
|
ForEach(items) { item in
|
|
ItemCard(item: item)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### LazyHGrid (iOS 14+)
|
|
|
|
Horizontal-scrolling grid. Define **rows**; columns grow unbounded.
|
|
|
|
```swift
|
|
let rows = [GridItem(.fixed(100)), GridItem(.fixed(100))]
|
|
|
|
ScrollView(.horizontal) {
|
|
LazyHGrid(rows: rows, spacing: 16) {
|
|
ForEach(items) { item in
|
|
ItemCard(item: item)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### GridItem.Size
|
|
|
|
| Size | Behavior |
|
|
|------|----------|
|
|
| `.fixed(CGFloat)` | Exact width/height |
|
|
| `.flexible(minimum:maximum:)` | Fills space equally |
|
|
| `.adaptive(minimum:maximum:)` | Creates as many as fit |
|
|
|
|
```swift
|
|
// Adaptive: responsive column count
|
|
let columns = [GridItem(.adaptive(minimum: 150))]
|
|
```
|
|
|
|
---
|
|
|
|
## Part 3: Outlines
|
|
|
|
### List with Hierarchical Data (iOS 14+)
|
|
|
|
```swift
|
|
struct FileItem: Identifiable {
|
|
let id = UUID()
|
|
var name: String
|
|
var children: [FileItem]? // nil = leaf
|
|
}
|
|
|
|
List(files, children: \.children) { file in
|
|
Label(file.name, systemImage: file.children != nil ? "folder" : "doc")
|
|
}
|
|
.listStyle(.sidebar)
|
|
```
|
|
|
|
### OutlineGroup (iOS 14+)
|
|
|
|
For custom hierarchical layouts outside List.
|
|
|
|
```swift
|
|
List {
|
|
ForEach(canvases) { canvas in
|
|
Section(header: Text(canvas.name)) {
|
|
OutlineGroup(canvas.graphics, children: \.children) { graphic in
|
|
GraphicRow(graphic: graphic)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### DisclosureGroup (iOS 14+)
|
|
|
|
```swift
|
|
@State private var isExpanded = false
|
|
|
|
DisclosureGroup("Advanced Options", isExpanded: $isExpanded) {
|
|
Toggle("Enable Feature", isOn: $feature)
|
|
Slider(value: $intensity)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Part 4: Common Patterns
|
|
|
|
### Photo Grid
|
|
|
|
```swift
|
|
let columns = [GridItem(.adaptive(minimum: 100), spacing: 2)]
|
|
|
|
ScrollView {
|
|
LazyVGrid(columns: columns, spacing: 2) {
|
|
ForEach(photos) { photo in
|
|
AsyncImage(url: photo.thumbnailURL) { image in
|
|
image.resizable().aspectRatio(1, contentMode: .fill)
|
|
} placeholder: { Color.gray }
|
|
.aspectRatio(1, contentMode: .fill)
|
|
.clipped()
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Horizontal Carousel
|
|
|
|
```swift
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
LazyHStack(spacing: 16) {
|
|
ForEach(items) { item in
|
|
CarouselCard(item: item).frame(width: 280)
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
}
|
|
```
|
|
|
|
### File Browser
|
|
|
|
```swift
|
|
List(selection: $selection) {
|
|
OutlineGroup(rootItems, children: \.children) { item in
|
|
Label {
|
|
Text(item.name)
|
|
} icon: {
|
|
Image(systemName: item.children != nil ? "folder.fill" : "doc.fill")
|
|
}
|
|
}
|
|
}
|
|
.listStyle(.sidebar)
|
|
```
|
|
|
|
---
|
|
|
|
## Part 5: Performance
|
|
|
|
### When to Use Lazy
|
|
|
|
| Size | Scrollable? | Use |
|
|
|------|-------------|-----|
|
|
| 1-20 | No | VStack/HStack |
|
|
| 1-20 | Yes | VStack/HStack in ScrollView |
|
|
| 20-100 | Yes | LazyVStack/LazyHStack |
|
|
| 100+ | Yes | LazyVStack/LazyHStack or List |
|
|
| Grid <50 | No | Grid |
|
|
| Grid 50+ | Yes | LazyVGrid/LazyHGrid |
|
|
|
|
**Cache GridItem arrays** — define outside body:
|
|
|
|
```swift
|
|
struct ContentView: View {
|
|
let columns = [GridItem(.adaptive(minimum: 150))] // ✅
|
|
var body: some View {
|
|
LazyVGrid(columns: columns) { ... }
|
|
}
|
|
}
|
|
```
|
|
|
|
### iOS 26 Performance
|
|
|
|
- **6x faster list loading** for 100k+ items
|
|
- **16x faster list updates**
|
|
- **Reduced dropped frames** in scrolling
|
|
- **Nested ScrollViews with lazy stacks** now properly defer loading:
|
|
|
|
```swift
|
|
ScrollView(.horizontal) {
|
|
LazyHStack {
|
|
ForEach(photoSets) { set in
|
|
ScrollView(.vertical) {
|
|
LazyVStack {
|
|
ForEach(set.photos) { PhotoView(photo: $0) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Part 6: Scroll Enhancements
|
|
|
|
### containerRelativeFrame (iOS 17+)
|
|
|
|
Size views relative to scroll container.
|
|
|
|
```swift
|
|
ScrollView(.horizontal) {
|
|
LazyHStack {
|
|
ForEach(cards) { card in
|
|
CardView(card: card)
|
|
.containerRelativeFrame(.horizontal, count: 3, span: 1, spacing: 16)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### scrollTargetLayout (iOS 17+)
|
|
|
|
Enable snapping.
|
|
|
|
```swift
|
|
ScrollView(.horizontal) {
|
|
LazyHStack {
|
|
ForEach(items) { ItemCard(item: $0) }
|
|
}
|
|
.scrollTargetLayout()
|
|
}
|
|
.scrollTargetBehavior(.viewAligned)
|
|
```
|
|
|
|
### scrollPosition (iOS 17+)
|
|
|
|
Track topmost visible item. **Requires `.id()` on each item.**
|
|
|
|
```swift
|
|
@State private var position: Item.ID?
|
|
|
|
ScrollView {
|
|
LazyVStack {
|
|
ForEach(items) { item in
|
|
ItemRow(item: item).id(item.id)
|
|
}
|
|
}
|
|
}
|
|
.scrollPosition(id: $position)
|
|
```
|
|
|
|
### scrollTransition (iOS 17+)
|
|
|
|
```swift
|
|
.scrollTransition { content, phase in
|
|
content
|
|
.opacity(1 - abs(phase.value) * 0.5)
|
|
.scaleEffect(phase.isIdentity ? 1.0 : 0.75)
|
|
}
|
|
```
|
|
|
|
### onScrollGeometryChange (iOS 18+)
|
|
|
|
```swift
|
|
.onScrollGeometryChange(for: Bool.self) { geo in
|
|
geo.contentOffset.y < geo.contentInsets.top
|
|
} action: { _, isTop in
|
|
showBackButton = !isTop
|
|
}
|
|
```
|
|
|
|
### onScrollVisibilityChange (iOS 18+)
|
|
|
|
```swift
|
|
VideoPlayer(player: player)
|
|
.onScrollVisibilityChange(threshold: 0.2) { visible in
|
|
visible ? player.play() : player.pause()
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Resources
|
|
|
|
**WWDC**: 2020-10031, 2022-10056, 2023-10148, 2024-10144, 2025-256
|
|
|
|
**Docs**: /swiftui/lazyvstack, /swiftui/lazyvgrid, /swiftui/lazyhgrid, /swiftui/grid, /swiftui/outlinegroup, /swiftui/disclosuregroup
|
|
|
|
**Skills**: axiom-swiftui-layout, axiom-swiftui-layout-ref, axiom-swiftui-nav, axiom-swiftui-26-ref
|