@kata-sh/cli 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/app-paths.d.ts +4 -0
- package/dist/app-paths.js +6 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +56 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +95 -0
- package/dist/resource-loader.d.ts +18 -0
- package/dist/resource-loader.js +50 -0
- package/dist/wizard.d.ts +15 -0
- package/dist/wizard.js +159 -0
- package/package.json +50 -21
- package/pkg/dist/modes/interactive/theme/dark.json +85 -0
- package/pkg/dist/modes/interactive/theme/light.json +84 -0
- package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/pkg/dist/modes/interactive/theme/theme.js +949 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
- package/pkg/package.json +8 -0
- package/scripts/postinstall.js +45 -0
- package/src/resources/AGENTS.md +108 -0
- package/src/resources/KATA-WORKFLOW.md +661 -0
- package/src/resources/agents/researcher.md +29 -0
- package/src/resources/agents/scout.md +56 -0
- package/src/resources/agents/worker.md +31 -0
- package/src/resources/extensions/ask-user-questions.ts +200 -0
- package/src/resources/extensions/bg-shell/index.ts +2758 -0
- package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
- package/src/resources/extensions/browser-tools/core.js +1057 -0
- package/src/resources/extensions/browser-tools/index.ts +4916 -0
- package/src/resources/extensions/browser-tools/package.json +20 -0
- package/src/resources/extensions/context7/index.ts +428 -0
- package/src/resources/extensions/context7/package.json +11 -0
- package/src/resources/extensions/get-secrets-from-user.ts +352 -0
- package/src/resources/extensions/github/formatters.ts +207 -0
- package/src/resources/extensions/github/gh-api.ts +537 -0
- package/src/resources/extensions/github/index.ts +778 -0
- package/src/resources/extensions/kata/activity-log.ts +88 -0
- package/src/resources/extensions/kata/auto.ts +2786 -0
- package/src/resources/extensions/kata/commands.ts +355 -0
- package/src/resources/extensions/kata/crash-recovery.ts +85 -0
- package/src/resources/extensions/kata/dashboard-overlay.ts +516 -0
- package/src/resources/extensions/kata/docs/preferences-reference.md +103 -0
- package/src/resources/extensions/kata/doctor.ts +683 -0
- package/src/resources/extensions/kata/files.ts +730 -0
- package/src/resources/extensions/kata/gitignore.ts +165 -0
- package/src/resources/extensions/kata/guided-flow.ts +976 -0
- package/src/resources/extensions/kata/index.ts +556 -0
- package/src/resources/extensions/kata/metrics.ts +397 -0
- package/src/resources/extensions/kata/observability-validator.ts +408 -0
- package/src/resources/extensions/kata/package.json +11 -0
- package/src/resources/extensions/kata/paths.ts +346 -0
- package/src/resources/extensions/kata/preferences.ts +695 -0
- package/src/resources/extensions/kata/prompt-loader.ts +50 -0
- package/src/resources/extensions/kata/prompts/complete-milestone.md +25 -0
- package/src/resources/extensions/kata/prompts/complete-slice.md +27 -0
- package/src/resources/extensions/kata/prompts/discuss.md +151 -0
- package/src/resources/extensions/kata/prompts/doctor-heal.md +29 -0
- package/src/resources/extensions/kata/prompts/execute-task.md +64 -0
- package/src/resources/extensions/kata/prompts/guided-complete-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-milestone.md +3 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-slice.md +59 -0
- package/src/resources/extensions/kata/prompts/guided-execute-task.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-plan-milestone.md +23 -0
- package/src/resources/extensions/kata/prompts/guided-plan-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-research-slice.md +11 -0
- package/src/resources/extensions/kata/prompts/guided-resume-task.md +1 -0
- package/src/resources/extensions/kata/prompts/plan-milestone.md +47 -0
- package/src/resources/extensions/kata/prompts/plan-slice.md +63 -0
- package/src/resources/extensions/kata/prompts/queue.md +85 -0
- package/src/resources/extensions/kata/prompts/reassess-roadmap.md +48 -0
- package/src/resources/extensions/kata/prompts/replan-slice.md +39 -0
- package/src/resources/extensions/kata/prompts/research-milestone.md +37 -0
- package/src/resources/extensions/kata/prompts/research-slice.md +28 -0
- package/src/resources/extensions/kata/prompts/run-uat.md +109 -0
- package/src/resources/extensions/kata/prompts/system.md +341 -0
- package/src/resources/extensions/kata/session-forensics.ts +550 -0
- package/src/resources/extensions/kata/skill-discovery.ts +137 -0
- package/src/resources/extensions/kata/state.ts +509 -0
- package/src/resources/extensions/kata/templates/context.md +76 -0
- package/src/resources/extensions/kata/templates/decisions.md +8 -0
- package/src/resources/extensions/kata/templates/milestone-summary.md +73 -0
- package/src/resources/extensions/kata/templates/plan.md +133 -0
- package/src/resources/extensions/kata/templates/preferences.md +15 -0
- package/src/resources/extensions/kata/templates/project.md +31 -0
- package/src/resources/extensions/kata/templates/reassessment.md +28 -0
- package/src/resources/extensions/kata/templates/requirements.md +81 -0
- package/src/resources/extensions/kata/templates/research.md +46 -0
- package/src/resources/extensions/kata/templates/roadmap.md +118 -0
- package/src/resources/extensions/kata/templates/slice-context.md +58 -0
- package/src/resources/extensions/kata/templates/slice-summary.md +99 -0
- package/src/resources/extensions/kata/templates/state.md +19 -0
- package/src/resources/extensions/kata/templates/task-plan.md +52 -0
- package/src/resources/extensions/kata/templates/task-summary.md +57 -0
- package/src/resources/extensions/kata/templates/uat.md +54 -0
- package/src/resources/extensions/kata/tests/activity-log-prune.test.ts +327 -0
- package/src/resources/extensions/kata/tests/auto-preflight.test.ts +97 -0
- package/src/resources/extensions/kata/tests/auto-supervisor.test.mjs +53 -0
- package/src/resources/extensions/kata/tests/complete-milestone.test.ts +317 -0
- package/src/resources/extensions/kata/tests/cost-projection.test.ts +160 -0
- package/src/resources/extensions/kata/tests/derive-state-deps.test.ts +477 -0
- package/src/resources/extensions/kata/tests/derive-state.test.ts +1013 -0
- package/src/resources/extensions/kata/tests/doctor.test.ts +718 -0
- package/src/resources/extensions/kata/tests/idle-recovery.test.ts +490 -0
- package/src/resources/extensions/kata/tests/metrics-io.test.ts +254 -0
- package/src/resources/extensions/kata/tests/metrics.test.ts +217 -0
- package/src/resources/extensions/kata/tests/must-have-parser.test.ts +309 -0
- package/src/resources/extensions/kata/tests/parsers.test.ts +1257 -0
- package/src/resources/extensions/kata/tests/plan-milestone.test.ts +185 -0
- package/src/resources/extensions/kata/tests/plan-quality-validator.test.ts +386 -0
- package/src/resources/extensions/kata/tests/reassess-prompt.test.ts +208 -0
- package/src/resources/extensions/kata/tests/replan-slice.test.ts +686 -0
- package/src/resources/extensions/kata/tests/requirements.test.ts +151 -0
- package/src/resources/extensions/kata/tests/resolve-ts-hooks.mjs +17 -0
- package/src/resources/extensions/kata/tests/resolve-ts.mjs +11 -0
- package/src/resources/extensions/kata/tests/run-uat.test.ts +383 -0
- package/src/resources/extensions/kata/tests/unit-runtime.test.ts +388 -0
- package/src/resources/extensions/kata/tests/workspace-index.test.ts +118 -0
- package/src/resources/extensions/kata/tests/worktree.test.ts +222 -0
- package/src/resources/extensions/kata/types.ts +159 -0
- package/src/resources/extensions/kata/unit-runtime.ts +163 -0
- package/src/resources/extensions/kata/workspace-index.ts +203 -0
- package/src/resources/extensions/kata/worktree.ts +182 -0
- package/src/resources/extensions/mac-tools/index.ts +852 -0
- package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
- package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
- package/src/resources/extensions/search-the-web/cache.ts +78 -0
- package/src/resources/extensions/search-the-web/format.ts +258 -0
- package/src/resources/extensions/search-the-web/http.ts +238 -0
- package/src/resources/extensions/search-the-web/index.ts +68 -0
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +404 -0
- package/src/resources/extensions/search-the-web/tool-search.ts +503 -0
- package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
- package/src/resources/extensions/shared/confirm-ui.ts +126 -0
- package/src/resources/extensions/shared/interview-ui.ts +822 -0
- package/src/resources/extensions/shared/next-action-ui.ts +235 -0
- package/src/resources/extensions/shared/progress-widget.ts +282 -0
- package/src/resources/extensions/shared/thinking-widget.ts +107 -0
- package/src/resources/extensions/shared/ui.ts +400 -0
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/slash-commands/audit.ts +92 -0
- package/src/resources/extensions/slash-commands/create-extension.ts +375 -0
- package/src/resources/extensions/slash-commands/create-slash-command.ts +280 -0
- package/src/resources/extensions/slash-commands/index.ts +12 -0
- package/src/resources/extensions/slash-commands/kata-run.ts +34 -0
- package/src/resources/extensions/subagent/agents.ts +126 -0
- package/src/resources/extensions/subagent/index.ts +1293 -0
- package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
- package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
- package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
- package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
- package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
- package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
- package/src/resources/skills/frontend-design/SKILL.md +45 -0
- package/src/resources/skills/swiftui/SKILL.md +208 -0
- package/src/resources/skills/swiftui/references/animations.md +921 -0
- package/src/resources/skills/swiftui/references/architecture.md +1561 -0
- package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
- package/src/resources/skills/swiftui/references/navigation.md +1492 -0
- package/src/resources/skills/swiftui/references/networking-async.md +214 -0
- package/src/resources/skills/swiftui/references/performance.md +1706 -0
- package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
- package/src/resources/skills/swiftui/references/state-management.md +1443 -0
- package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
- package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
- package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
- package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
- package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
- package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
- package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
- package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
- package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
- package/dist/commands/task.d.ts +0 -9
- package/dist/commands/task.d.ts.map +0 -1
- package/dist/commands/task.js +0 -129
- package/dist/commands/task.js.map +0 -1
- package/dist/commands/task.test.d.ts +0 -2
- package/dist/commands/task.test.d.ts.map +0 -1
- package/dist/commands/task.test.js +0 -169
- package/dist/commands/task.test.js.map +0 -1
- package/dist/e2e/task-e2e.test.d.ts +0 -2
- package/dist/e2e/task-e2e.test.d.ts.map +0 -1
- package/dist/e2e/task-e2e.test.js +0 -173
- package/dist/e2e/task-e2e.test.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -93
- package/dist/index.js.map +0 -1
- package/dist/slug.d.ts +0 -2
- package/dist/slug.d.ts.map +0 -1
- package/dist/slug.js +0 -12
- package/dist/slug.js.map +0 -1
- package/dist/slug.test.d.ts +0 -2
- package/dist/slug.test.d.ts.map +0 -1
- package/dist/slug.test.js +0 -32
- package/dist/slug.test.js.map +0 -1
|
@@ -0,0 +1,1186 @@
|
|
|
1
|
+
<overview>
|
|
2
|
+
SwiftUI's layout system operates fundamentally differently from UIKit/Auto Layout. Instead of constraints, SwiftUI uses a **propose-measure-place** model:
|
|
3
|
+
|
|
4
|
+
1. **Propose**: Parent offers child a size
|
|
5
|
+
2. **Measure**: Child chooses its own size (parent must respect this)
|
|
6
|
+
3. **Place**: Parent positions child in its coordinate space
|
|
7
|
+
|
|
8
|
+
This creates a declarative, predictable layout system where conflicts are impossible. SwiftUI always produces a valid layout.
|
|
9
|
+
|
|
10
|
+
**Read this file when:**
|
|
11
|
+
- Choosing between layout containers (HStack, VStack, Grid, etc.)
|
|
12
|
+
- Dealing with complex positioning requirements
|
|
13
|
+
- Performance tuning layouts with large datasets
|
|
14
|
+
- Understanding GeometryReader usage and alternatives
|
|
15
|
+
|
|
16
|
+
**See also:**
|
|
17
|
+
- `performance.md` for layout performance optimization strategies
|
|
18
|
+
- `architecture.md` for structuring complex view hierarchies
|
|
19
|
+
</overview>
|
|
20
|
+
|
|
21
|
+
<layout_containers>
|
|
22
|
+
## Layout Containers
|
|
23
|
+
|
|
24
|
+
<container name="HStack">
|
|
25
|
+
**Purpose:** Horizontal arrangement of views from left to right (or right to left in RTL languages)
|
|
26
|
+
|
|
27
|
+
**Behavior:** Proposes equal width to all children, then distributes remaining space based on flexibility. Children choose their own heights.
|
|
28
|
+
|
|
29
|
+
**Alignment:** Default is `.center` vertically. Options: `.top`, `.center`, `.bottom`, `.firstTextBaseline`, `.lastTextBaseline`
|
|
30
|
+
|
|
31
|
+
**Spacing:** Default is system-defined (typically 8pt). Override with `spacing:` parameter or `.none` for zero spacing.
|
|
32
|
+
|
|
33
|
+
```swift
|
|
34
|
+
// Common usage with custom spacing and alignment
|
|
35
|
+
HStack(alignment: .top, spacing: 12) {
|
|
36
|
+
Image(systemName: "person.circle")
|
|
37
|
+
.font(.largeTitle)
|
|
38
|
+
|
|
39
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
40
|
+
Text("Username")
|
|
41
|
+
.font(.headline)
|
|
42
|
+
Text("Online")
|
|
43
|
+
.font(.caption)
|
|
44
|
+
.foregroundStyle(.secondary)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Spacer() // Pushes content to leading edge
|
|
48
|
+
|
|
49
|
+
Button("Follow") { }
|
|
50
|
+
}
|
|
51
|
+
.padding()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Performance:** Lightweight. All children are created immediately (not lazy).
|
|
55
|
+
</container>
|
|
56
|
+
|
|
57
|
+
<container name="VStack">
|
|
58
|
+
**Purpose:** Vertical arrangement of views from top to bottom
|
|
59
|
+
|
|
60
|
+
**Behavior:** Proposes equal height to all children, then distributes remaining space. Children choose their own widths.
|
|
61
|
+
|
|
62
|
+
**Alignment:** Default is `.center` horizontally. Options: `.leading`, `.center`, `.trailing`
|
|
63
|
+
|
|
64
|
+
**Spacing:** Default is system-defined. Override with `spacing:` parameter.
|
|
65
|
+
|
|
66
|
+
```swift
|
|
67
|
+
// Card layout with multiple sections
|
|
68
|
+
VStack(alignment: .leading, spacing: 16) {
|
|
69
|
+
Text("Title")
|
|
70
|
+
.font(.headline)
|
|
71
|
+
|
|
72
|
+
Text("Body text that can span multiple lines and will wrap naturally within the available width.")
|
|
73
|
+
.font(.body)
|
|
74
|
+
.foregroundStyle(.secondary)
|
|
75
|
+
|
|
76
|
+
HStack {
|
|
77
|
+
Spacer()
|
|
78
|
+
Button("Action") { }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
.padding()
|
|
82
|
+
.background(.background.secondary)
|
|
83
|
+
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Performance:** Lightweight. All children are created immediately.
|
|
87
|
+
</container>
|
|
88
|
+
|
|
89
|
+
<container name="ZStack">
|
|
90
|
+
**Purpose:** Layering views on the Z-axis (depth), drawing from back to front
|
|
91
|
+
|
|
92
|
+
**Behavior:** Proposes full available size to all children. Final size is the union of all child sizes. Later views draw on top of earlier views.
|
|
93
|
+
|
|
94
|
+
**Alignment:** Default is `.center` both horizontally and vertically. Options include `.topLeading`, `.bottomTrailing`, etc.
|
|
95
|
+
|
|
96
|
+
```swift
|
|
97
|
+
// Profile picture with badge
|
|
98
|
+
ZStack(alignment: .bottomTrailing) {
|
|
99
|
+
AsyncImage(url: profileURL) { image in
|
|
100
|
+
image
|
|
101
|
+
.resizable()
|
|
102
|
+
.scaledToFill()
|
|
103
|
+
} placeholder: {
|
|
104
|
+
Color.gray
|
|
105
|
+
}
|
|
106
|
+
.frame(width: 100, height: 100)
|
|
107
|
+
.clipShape(Circle())
|
|
108
|
+
|
|
109
|
+
// Notification badge
|
|
110
|
+
Circle()
|
|
111
|
+
.fill(.red)
|
|
112
|
+
.frame(width: 24, height: 24)
|
|
113
|
+
.overlay {
|
|
114
|
+
Text("3")
|
|
115
|
+
.font(.caption2.bold())
|
|
116
|
+
.foregroundStyle(.white)
|
|
117
|
+
}
|
|
118
|
+
.offset(x: 4, y: 4)
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Performance:** Lightweight. Avoid excessive layering for complex effects (use `.overlay()` or `.background()` instead when appropriate).
|
|
123
|
+
</container>
|
|
124
|
+
|
|
125
|
+
<container name="LazyVStack">
|
|
126
|
+
**Purpose:** Vertical stack with deferred view creation - only creates views when they scroll into view
|
|
127
|
+
|
|
128
|
+
**When to use:**
|
|
129
|
+
- Lists with hundreds or thousands of items
|
|
130
|
+
- Items with expensive initialization (images, complex views)
|
|
131
|
+
- Memory-constrained scenarios
|
|
132
|
+
|
|
133
|
+
**Difference from VStack:**
|
|
134
|
+
- VStack creates all children immediately
|
|
135
|
+
- LazyVStack creates children on-demand as they appear
|
|
136
|
+
- LazyVStack requires a ScrollView parent
|
|
137
|
+
- LazyVStack calculates layout incrementally
|
|
138
|
+
|
|
139
|
+
```swift
|
|
140
|
+
ScrollView {
|
|
141
|
+
LazyVStack(spacing: 12, pinnedViews: [.sectionHeaders]) {
|
|
142
|
+
Section {
|
|
143
|
+
ForEach(items) { item in
|
|
144
|
+
ItemRow(item: item)
|
|
145
|
+
.frame(height: 60)
|
|
146
|
+
}
|
|
147
|
+
} header: {
|
|
148
|
+
Text("Section Header")
|
|
149
|
+
.font(.headline)
|
|
150
|
+
.padding()
|
|
151
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
152
|
+
.background(.ultraThinMaterial)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Performance:**
|
|
159
|
+
- Superior for long lists (only renders visible views)
|
|
160
|
+
- Slight overhead per view creation
|
|
161
|
+
- Use `.id()` modifier to control view identity/reuse
|
|
162
|
+
|
|
163
|
+
**See also:** LazyHStack for horizontal lazy loading
|
|
164
|
+
</container>
|
|
165
|
+
|
|
166
|
+
<container name="LazyVGrid / LazyHGrid">
|
|
167
|
+
**Purpose:** Grid layouts with lazy loading - creates cells only when visible
|
|
168
|
+
|
|
169
|
+
**GridItem types:**
|
|
170
|
+
- `.fixed(width)`: Exactly the specified width
|
|
171
|
+
- `.flexible(minimum:maximum:)`: Grows to fill space within bounds
|
|
172
|
+
- `.adaptive(minimum:maximum:)`: Creates as many columns as fit
|
|
173
|
+
|
|
174
|
+
```swift
|
|
175
|
+
// Photo grid with adaptive columns
|
|
176
|
+
ScrollView {
|
|
177
|
+
LazyVGrid(
|
|
178
|
+
columns: [GridItem(.adaptive(minimum: 120, maximum: 200))],
|
|
179
|
+
spacing: 12
|
|
180
|
+
) {
|
|
181
|
+
ForEach(photos) { photo in
|
|
182
|
+
AsyncImage(url: photo.url) { image in
|
|
183
|
+
image
|
|
184
|
+
.resizable()
|
|
185
|
+
.scaledToFill()
|
|
186
|
+
} placeholder: {
|
|
187
|
+
Color.gray.opacity(0.3)
|
|
188
|
+
}
|
|
189
|
+
.frame(height: 120)
|
|
190
|
+
.clipShape(RoundedRectangle(cornerRadius: 8))
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
.padding()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Fixed column layout (3 columns)
|
|
197
|
+
let columns = [
|
|
198
|
+
GridItem(.flexible()),
|
|
199
|
+
GridItem(.flexible()),
|
|
200
|
+
GridItem(.flexible())
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
LazyVGrid(columns: columns, spacing: 16) {
|
|
204
|
+
ForEach(items) { item in
|
|
205
|
+
ItemCard(item: item)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Performance:**
|
|
211
|
+
- Only creates visible cells (significant memory savings)
|
|
212
|
+
- Best for image galleries, product catalogs, large datasets
|
|
213
|
+
- On macOS, performance may drop below UIKit CollectionView for very large datasets (see performance.md)
|
|
214
|
+
- Consider pagination for datasets over 1000 items
|
|
215
|
+
|
|
216
|
+
**When NOT to use:** Small grids (< 20 items) - use Grid instead for simpler code
|
|
217
|
+
</container>
|
|
218
|
+
|
|
219
|
+
<container name="Grid (iOS 16+)">
|
|
220
|
+
**Purpose:** Non-lazy grid with explicit row/column control and advanced alignment
|
|
221
|
+
|
|
222
|
+
**When to use:**
|
|
223
|
+
- Small datasets where all items can be in memory
|
|
224
|
+
- Need precise row/column control
|
|
225
|
+
- Need GridRow for custom row styling
|
|
226
|
+
- Need alignment across cells in different rows
|
|
227
|
+
|
|
228
|
+
**Difference from LazyVGrid:**
|
|
229
|
+
- Grid creates all cells immediately
|
|
230
|
+
- Grid gives more layout control (GridRow, cell spanning)
|
|
231
|
+
- Grid supports baseline alignment across rows
|
|
232
|
+
- Grid is simpler for static content
|
|
233
|
+
|
|
234
|
+
```swift
|
|
235
|
+
// Form-like layout with alignment
|
|
236
|
+
Grid(alignment: .leading, horizontalSpacing: 12, verticalSpacing: 8) {
|
|
237
|
+
GridRow {
|
|
238
|
+
Text("Name:")
|
|
239
|
+
.gridColumnAlignment(.trailing)
|
|
240
|
+
TextField("Enter name", text: $name)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
GridRow {
|
|
244
|
+
Text("Email:")
|
|
245
|
+
.gridColumnAlignment(.trailing)
|
|
246
|
+
TextField("Enter email", text: $email)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
GridRow {
|
|
250
|
+
Color.clear
|
|
251
|
+
.gridCellUnsizedAxes(.horizontal)
|
|
252
|
+
|
|
253
|
+
Button("Submit") { }
|
|
254
|
+
.gridCellColumns(1)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
.padding()
|
|
258
|
+
|
|
259
|
+
// Spanning cells
|
|
260
|
+
Grid {
|
|
261
|
+
GridRow {
|
|
262
|
+
Text("Header")
|
|
263
|
+
.gridCellColumns(3) // Spans 3 columns
|
|
264
|
+
.font(.headline)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
GridRow {
|
|
268
|
+
ForEach(1...3, id: \.self) { num in
|
|
269
|
+
Text("Cell \(num)")
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Performance:** All cells created immediately. Keep under 100 items or use LazyVGrid.
|
|
276
|
+
</container>
|
|
277
|
+
</layout_containers>
|
|
278
|
+
|
|
279
|
+
<geometry_reader>
|
|
280
|
+
## GeometryReader
|
|
281
|
+
|
|
282
|
+
**Purpose:** Access parent's proposed size and safe area insets for custom layout calculations
|
|
283
|
+
|
|
284
|
+
**When to use:**
|
|
285
|
+
- Custom drawing or graphics that need exact dimensions
|
|
286
|
+
- Complex animations requiring precise positioning
|
|
287
|
+
- Creating custom layout effects not possible with standard containers
|
|
288
|
+
- Reading coordinate spaces for gesture calculations
|
|
289
|
+
|
|
290
|
+
**When NOT to use:**
|
|
291
|
+
- Simple relative sizing → use `.frame(maxWidth: .infinity)` or `Spacer()`
|
|
292
|
+
- Container-relative frames → use `containerRelativeFrame()` (iOS 17+)
|
|
293
|
+
- Adaptive layouts → use `ViewThatFits` (iOS 16+)
|
|
294
|
+
- Safe area queries → use safe area modifiers directly
|
|
295
|
+
|
|
296
|
+
```swift
|
|
297
|
+
// Correct usage: Custom circular progress
|
|
298
|
+
GeometryReader { geometry in
|
|
299
|
+
ZStack {
|
|
300
|
+
Circle()
|
|
301
|
+
.stroke(Color.gray.opacity(0.3), lineWidth: 10)
|
|
302
|
+
|
|
303
|
+
Circle()
|
|
304
|
+
.trim(from: 0, to: progress)
|
|
305
|
+
.stroke(Color.blue, style: StrokeStyle(lineWidth: 10, lineCap: .round))
|
|
306
|
+
.rotationEffect(.degrees(-90))
|
|
307
|
+
.animation(.easeInOut, value: progress)
|
|
308
|
+
}
|
|
309
|
+
.frame(width: geometry.size.width, height: geometry.size.width)
|
|
310
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
311
|
+
}
|
|
312
|
+
.aspectRatio(1, contentMode: .fit)
|
|
313
|
+
|
|
314
|
+
// Correct usage: Reading coordinate spaces
|
|
315
|
+
struct DragView: View {
|
|
316
|
+
@State private var location: CGPoint = .zero
|
|
317
|
+
|
|
318
|
+
var body: some View {
|
|
319
|
+
GeometryReader { geometry in
|
|
320
|
+
Circle()
|
|
321
|
+
.fill(.blue)
|
|
322
|
+
.frame(width: 50, height: 50)
|
|
323
|
+
.position(location)
|
|
324
|
+
.gesture(
|
|
325
|
+
DragGesture(coordinateSpace: .named("container"))
|
|
326
|
+
.onChanged { value in
|
|
327
|
+
location = value.location
|
|
328
|
+
}
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
.coordinateSpace(name: "container")
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Pitfalls:**
|
|
337
|
+
|
|
338
|
+
1. **Expands to fill all available space**: GeometryReader acts like `Color.clear.frame(maxWidth: .infinity, maxHeight: .infinity)`, which breaks layouts in ScrollViews and can cause infinite height calculations
|
|
339
|
+
|
|
340
|
+
2. **Breaks ScrollView behavior**: Inside ScrollView, GeometryReader tries to fill infinite space, causing layout loops and broken scrolling
|
|
341
|
+
|
|
342
|
+
3. **Overused for simple tasks**: Most GeometryReader usage can be replaced with simpler solutions
|
|
343
|
+
|
|
344
|
+
4. **Deep nesting causes unpredictable behavior**: Nested GeometryReaders compound layout issues
|
|
345
|
+
|
|
346
|
+
5. **Performance overhead**: Rebuilds view on every geometry change
|
|
347
|
+
|
|
348
|
+
**Alternatives to GeometryReader:**
|
|
349
|
+
|
|
350
|
+
```swift
|
|
351
|
+
// ❌ BAD: Using GeometryReader for container-relative sizing
|
|
352
|
+
GeometryReader { geometry in
|
|
353
|
+
Rectangle()
|
|
354
|
+
.frame(width: geometry.size.width * 0.5)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ✅ GOOD: Use containerRelativeFrame (iOS 17+)
|
|
358
|
+
Rectangle()
|
|
359
|
+
.containerRelativeFrame(.horizontal) { width, _ in
|
|
360
|
+
width * 0.5
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ✅ GOOD: Use frame modifiers
|
|
364
|
+
Rectangle()
|
|
365
|
+
.frame(maxWidth: .infinity)
|
|
366
|
+
.padding(.horizontal, .infinity) // Creates 50% width
|
|
367
|
+
|
|
368
|
+
// ❌ BAD: GeometryReader for adaptive layouts
|
|
369
|
+
GeometryReader { geometry in
|
|
370
|
+
if geometry.size.width > 600 {
|
|
371
|
+
HStack { content }
|
|
372
|
+
} else {
|
|
373
|
+
VStack { content }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ✅ GOOD: Use ViewThatFits (iOS 16+)
|
|
378
|
+
ViewThatFits {
|
|
379
|
+
HStack { content }
|
|
380
|
+
VStack { content }
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Using GeometryReader safely:**
|
|
385
|
+
|
|
386
|
+
```swift
|
|
387
|
+
// Use in .background() or .overlay() to avoid affecting layout
|
|
388
|
+
Text("Hello")
|
|
389
|
+
.background(
|
|
390
|
+
GeometryReader { geometry in
|
|
391
|
+
Color.clear
|
|
392
|
+
.onAppear {
|
|
393
|
+
print("Size: \(geometry.size)")
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
)
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
**See also:** `performance.md` for GeometryReader performance considerations
|
|
400
|
+
</geometry_reader>
|
|
401
|
+
|
|
402
|
+
<custom_layout>
|
|
403
|
+
## Custom Layout Protocol (iOS 16+)
|
|
404
|
+
|
|
405
|
+
**When to use:**
|
|
406
|
+
- Standard containers cannot achieve the desired layout
|
|
407
|
+
- Flow/tag layouts (wrapping items like a text paragraph)
|
|
408
|
+
- Radial/circular arrangements
|
|
409
|
+
- Custom grid behaviors (masonry, Pinterest-style)
|
|
410
|
+
- Complex alignment requirements across multiple views
|
|
411
|
+
|
|
412
|
+
**Protocol requirements:**
|
|
413
|
+
1. `sizeThatFits(proposal:subviews:cache:)`: Calculate and return container size
|
|
414
|
+
2. `placeSubviews(in:proposal:subviews:cache:)`: Position each subview
|
|
415
|
+
|
|
416
|
+
**Optional:**
|
|
417
|
+
- `makeCache(subviews:)`: Create shared computation cache
|
|
418
|
+
- `updateCache(_:subviews:)`: Update cache when subviews change
|
|
419
|
+
- `explicitAlignment(of:in:proposal:subviews:cache:)`: Define custom alignment guides
|
|
420
|
+
|
|
421
|
+
```swift
|
|
422
|
+
// Complete FlowLayout example (tag cloud, wrapping items)
|
|
423
|
+
struct FlowLayout: Layout {
|
|
424
|
+
var spacing: CGFloat = 8
|
|
425
|
+
|
|
426
|
+
func sizeThatFits(
|
|
427
|
+
proposal: ProposedViewSize,
|
|
428
|
+
subviews: Subviews,
|
|
429
|
+
cache: inout Cache
|
|
430
|
+
) -> CGSize {
|
|
431
|
+
let rows = computeRows(proposal: proposal, subviews: subviews)
|
|
432
|
+
|
|
433
|
+
let width = proposal.replacingUnspecifiedDimensions().width
|
|
434
|
+
let height = rows.reduce(0) { $0 + $1.height + spacing } - spacing
|
|
435
|
+
|
|
436
|
+
return CGSize(width: width, height: height)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
func placeSubviews(
|
|
440
|
+
in bounds: CGRect,
|
|
441
|
+
proposal: ProposedViewSize,
|
|
442
|
+
subviews: Subviews,
|
|
443
|
+
cache: inout Cache
|
|
444
|
+
) {
|
|
445
|
+
let rows = computeRows(proposal: proposal, subviews: subviews)
|
|
446
|
+
|
|
447
|
+
var y = bounds.minY
|
|
448
|
+
for row in rows {
|
|
449
|
+
var x = bounds.minX
|
|
450
|
+
|
|
451
|
+
for index in row.subviewIndices {
|
|
452
|
+
let subview = subviews[index]
|
|
453
|
+
let size = subview.sizeThatFits(.unspecified)
|
|
454
|
+
|
|
455
|
+
subview.place(
|
|
456
|
+
at: CGPoint(x: x, y: y),
|
|
457
|
+
proposal: ProposedViewSize(size)
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
x += size.width + spacing
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
y += row.height + spacing
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Cache structure for performance
|
|
468
|
+
struct Cache {
|
|
469
|
+
var rows: [Row] = []
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
struct Row {
|
|
473
|
+
var subviewIndices: [Int]
|
|
474
|
+
var height: CGFloat
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private func computeRows(
|
|
478
|
+
proposal: ProposedViewSize,
|
|
479
|
+
subviews: Subviews
|
|
480
|
+
) -> [Row] {
|
|
481
|
+
let width = proposal.replacingUnspecifiedDimensions().width
|
|
482
|
+
var rows: [Row] = []
|
|
483
|
+
var currentRow: [Int] = []
|
|
484
|
+
var currentX: CGFloat = 0
|
|
485
|
+
var currentHeight: CGFloat = 0
|
|
486
|
+
|
|
487
|
+
for (index, subview) in subviews.enumerated() {
|
|
488
|
+
let size = subview.sizeThatFits(.unspecified)
|
|
489
|
+
|
|
490
|
+
if currentX + size.width > width && !currentRow.isEmpty {
|
|
491
|
+
// Start new row
|
|
492
|
+
rows.append(Row(subviewIndices: currentRow, height: currentHeight))
|
|
493
|
+
currentRow = []
|
|
494
|
+
currentX = 0
|
|
495
|
+
currentHeight = 0
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
currentRow.append(index)
|
|
499
|
+
currentX += size.width + spacing
|
|
500
|
+
currentHeight = max(currentHeight, size.height)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if !currentRow.isEmpty {
|
|
504
|
+
rows.append(Row(subviewIndices: currentRow, height: currentHeight))
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return rows
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Usage
|
|
512
|
+
FlowLayout(spacing: 12) {
|
|
513
|
+
ForEach(tags, id: \.self) { tag in
|
|
514
|
+
Text(tag)
|
|
515
|
+
.padding(.horizontal, 12)
|
|
516
|
+
.padding(.vertical, 6)
|
|
517
|
+
.background(.blue.opacity(0.2))
|
|
518
|
+
.clipShape(Capsule())
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Radial layout example
|
|
523
|
+
struct RadialLayout: Layout {
|
|
524
|
+
func sizeThatFits(
|
|
525
|
+
proposal: ProposedViewSize,
|
|
526
|
+
subviews: Subviews,
|
|
527
|
+
cache: inout ()
|
|
528
|
+
) -> CGSize {
|
|
529
|
+
proposal.replacingUnspecifiedDimensions()
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
func placeSubviews(
|
|
533
|
+
in bounds: CGRect,
|
|
534
|
+
proposal: ProposedViewSize,
|
|
535
|
+
subviews: Subviews,
|
|
536
|
+
cache: inout ()
|
|
537
|
+
) {
|
|
538
|
+
let radius = min(bounds.width, bounds.height) / 2.5
|
|
539
|
+
let center = CGPoint(x: bounds.midX, y: bounds.midY)
|
|
540
|
+
let angle = (2 * .pi) / Double(subviews.count)
|
|
541
|
+
|
|
542
|
+
for (index, subview) in subviews.enumerated() {
|
|
543
|
+
let theta = angle * Double(index) - .pi / 2
|
|
544
|
+
let x = center.x + radius * cos(theta)
|
|
545
|
+
let y = center.y + radius * sin(theta)
|
|
546
|
+
|
|
547
|
+
subview.place(
|
|
548
|
+
at: CGPoint(x: x, y: y),
|
|
549
|
+
anchor: .center,
|
|
550
|
+
proposal: .unspecified
|
|
551
|
+
)
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
**Use cases:**
|
|
558
|
+
- **Flow/tag layout**: Wrapping items like tags, badges, or chips
|
|
559
|
+
- **Radial layout**: Circular menu, dial controls
|
|
560
|
+
- **Masonry grid**: Pinterest-style uneven grid
|
|
561
|
+
- **Custom calendar**: Week views with variable heights
|
|
562
|
+
- **Waterfall layout**: Staggered grid with varying item heights
|
|
563
|
+
|
|
564
|
+
**See also:**
|
|
565
|
+
- Apple's official documentation: [Composing custom layouts with SwiftUI](https://developer.apple.com/documentation/swiftui/composing_custom_layouts_with_swiftui)
|
|
566
|
+
- `performance.md` for Layout protocol caching strategies
|
|
567
|
+
</custom_layout>
|
|
568
|
+
|
|
569
|
+
<alignment_guides>
|
|
570
|
+
## Alignment and Alignment Guides
|
|
571
|
+
|
|
572
|
+
**Built-in alignments:**
|
|
573
|
+
- **Vertical**: `.leading`, `.center`, `.trailing`
|
|
574
|
+
- **Horizontal**: `.top`, `.center`, `.bottom`, `.firstTextBaseline`, `.lastTextBaseline`
|
|
575
|
+
|
|
576
|
+
**How alignment works:** When containers like HStack or VStack align children, they use alignment guides. Each view exposes alignment guide values, and the container aligns those values across children.
|
|
577
|
+
|
|
578
|
+
**Custom alignment guides:**
|
|
579
|
+
|
|
580
|
+
```swift
|
|
581
|
+
// Define custom vertical alignment
|
|
582
|
+
extension VerticalAlignment {
|
|
583
|
+
private struct MidAccountAndName: AlignmentID {
|
|
584
|
+
static func defaultValue(in context: ViewDimensions) -> CGFloat {
|
|
585
|
+
context[VerticalAlignment.center]
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
static let midAccountAndName = VerticalAlignment(MidAccountAndName.self)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Use custom alignment
|
|
593
|
+
HStack(alignment: .midAccountAndName) {
|
|
594
|
+
VStack(alignment: .trailing) {
|
|
595
|
+
Text("Full Name:")
|
|
596
|
+
Text("Address:")
|
|
597
|
+
Text("Account Number:")
|
|
598
|
+
.alignmentGuide(.midAccountAndName) { d in
|
|
599
|
+
d[VerticalAlignment.center]
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
VStack(alignment: .leading) {
|
|
604
|
+
Text("John Doe")
|
|
605
|
+
Text("123 Main St")
|
|
606
|
+
Text("98765-4321")
|
|
607
|
+
.alignmentGuide(.midAccountAndName) { d in
|
|
608
|
+
d[VerticalAlignment.center]
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Adjusting alignment dynamically
|
|
614
|
+
Image(systemName: "arrow.up")
|
|
615
|
+
.alignmentGuide(.leading) { d in
|
|
616
|
+
d[.leading] - 50 // Shift left by 50 points
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
**Preference keys for custom alignment:**
|
|
621
|
+
|
|
622
|
+
Preference keys allow child views to communicate layout information up the hierarchy.
|
|
623
|
+
|
|
624
|
+
```swift
|
|
625
|
+
// Define preference key for collecting bounds
|
|
626
|
+
struct BoundsPreferenceKey: PreferenceKey {
|
|
627
|
+
static var defaultValue: [String: Anchor<CGRect>] = [:]
|
|
628
|
+
|
|
629
|
+
static func reduce(
|
|
630
|
+
value: inout [String: Anchor<CGRect>],
|
|
631
|
+
nextValue: () -> [String: Anchor<CGRect]]
|
|
632
|
+
) {
|
|
633
|
+
value.merge(nextValue(), uniquingKeysWith: { $1 })
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Use anchorPreference to collect child bounds
|
|
638
|
+
struct HighlightableView: View {
|
|
639
|
+
@State private var highlightedFrame: CGRect?
|
|
640
|
+
|
|
641
|
+
var body: some View {
|
|
642
|
+
VStack(spacing: 20) {
|
|
643
|
+
Text("First")
|
|
644
|
+
.padding()
|
|
645
|
+
.background(Color.blue.opacity(0.3))
|
|
646
|
+
.anchorPreference(
|
|
647
|
+
key: BoundsPreferenceKey.self,
|
|
648
|
+
value: .bounds
|
|
649
|
+
) {
|
|
650
|
+
["first": $0]
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
Text("Second")
|
|
654
|
+
.padding()
|
|
655
|
+
.background(Color.green.opacity(0.3))
|
|
656
|
+
.anchorPreference(
|
|
657
|
+
key: BoundsPreferenceKey.self,
|
|
658
|
+
value: .bounds
|
|
659
|
+
) {
|
|
660
|
+
["second": $0]
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
.overlayPreferenceValue(BoundsPreferenceKey.self) { preferences in
|
|
664
|
+
GeometryReader { geometry in
|
|
665
|
+
if let anchor = preferences["first"],
|
|
666
|
+
let frame = highlightedFrame {
|
|
667
|
+
Rectangle()
|
|
668
|
+
.stroke(Color.red, lineWidth: 2)
|
|
669
|
+
.frame(width: frame.width, height: frame.height)
|
|
670
|
+
.position(x: frame.midX, y: frame.midY)
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
.onPreferenceChange(BoundsPreferenceKey.self) { preferences in
|
|
675
|
+
// React to preference changes
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
**Practical use cases:**
|
|
682
|
+
- Aligning labels across separate VStacks
|
|
683
|
+
- Syncing baseline alignment in complex layouts
|
|
684
|
+
- Cross-highlighting related views
|
|
685
|
+
- Creating connection lines between views
|
|
686
|
+
|
|
687
|
+
**See also:**
|
|
688
|
+
- [The SwiftUI Lab: Alignment Guides](https://swiftui-lab.com/alignment-guides/)
|
|
689
|
+
- [Swift with Majid: Layout alignment](https://fatbobman.com/en/posts/layout-alignment/)
|
|
690
|
+
</alignment_guides>
|
|
691
|
+
|
|
692
|
+
<safe_areas>
|
|
693
|
+
## Safe Area Handling
|
|
694
|
+
|
|
695
|
+
SwiftUI respects safe areas by default (avoiding notches, home indicator, status bar).
|
|
696
|
+
|
|
697
|
+
**Three key modifiers:**
|
|
698
|
+
|
|
699
|
+
### `ignoresSafeArea(_:edges:)`
|
|
700
|
+
**When to use:** Extend backgrounds or images edge-to-edge while keeping content safe
|
|
701
|
+
|
|
702
|
+
```swift
|
|
703
|
+
// Background that extends to edges
|
|
704
|
+
ZStack {
|
|
705
|
+
Color.blue
|
|
706
|
+
.ignoresSafeArea() // Goes edge-to-edge
|
|
707
|
+
|
|
708
|
+
VStack {
|
|
709
|
+
Text("Content")
|
|
710
|
+
Spacer()
|
|
711
|
+
}
|
|
712
|
+
.padding() // Content stays in safe area
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Ignore only specific edges
|
|
716
|
+
ScrollView {
|
|
717
|
+
content
|
|
718
|
+
}
|
|
719
|
+
.ignoresSafeArea(.container, edges: .bottom)
|
|
720
|
+
|
|
721
|
+
// Ignore keyboard safe area
|
|
722
|
+
TextField("Message", text: $message)
|
|
723
|
+
.ignoresSafeArea(.keyboard)
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
**Regions:**
|
|
727
|
+
- `.container`: Device edges, status bar, home indicator
|
|
728
|
+
- `.keyboard`: Soft keyboard area
|
|
729
|
+
- `.all`: Both container and keyboard
|
|
730
|
+
|
|
731
|
+
**Edges:** `.top`, `.bottom`, `.leading`, `.trailing`, `.horizontal`, `.vertical`, `.all`
|
|
732
|
+
|
|
733
|
+
### `safeAreaInset(edge:alignment:spacing:content:)`
|
|
734
|
+
**When to use:** Add custom bars (toolbars, tab bars) that shrink the safe area for other content
|
|
735
|
+
|
|
736
|
+
```swift
|
|
737
|
+
// Custom bottom toolbar
|
|
738
|
+
ScrollView {
|
|
739
|
+
ForEach(items) { item in
|
|
740
|
+
ItemRow(item: item)
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
.safeAreaInset(edge: .bottom, spacing: 0) {
|
|
744
|
+
HStack {
|
|
745
|
+
Button("Action 1") { }
|
|
746
|
+
Spacer()
|
|
747
|
+
Button("Action 2") { }
|
|
748
|
+
}
|
|
749
|
+
.padding()
|
|
750
|
+
.background(.ultraThinMaterial)
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Multiple insets stack
|
|
754
|
+
ScrollView {
|
|
755
|
+
content
|
|
756
|
+
}
|
|
757
|
+
.safeAreaInset(edge: .top) {
|
|
758
|
+
SearchBar()
|
|
759
|
+
}
|
|
760
|
+
.safeAreaInset(edge: .bottom) {
|
|
761
|
+
BottomBar()
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Inset with custom alignment
|
|
765
|
+
List(messages) { message in
|
|
766
|
+
MessageRow(message: message)
|
|
767
|
+
}
|
|
768
|
+
.safeAreaInset(edge: .bottom, alignment: .trailing) {
|
|
769
|
+
Button(action: compose) {
|
|
770
|
+
Image(systemName: "plus.circle.fill")
|
|
771
|
+
.font(.largeTitle)
|
|
772
|
+
}
|
|
773
|
+
.padding()
|
|
774
|
+
}
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
**Key behavior:** Unlike `ignoresSafeArea`, this **shrinks** the safe area so other views avoid it.
|
|
778
|
+
|
|
779
|
+
### `safeAreaPadding(_:_:)` (iOS 17+)
|
|
780
|
+
**When to use:** Extend safe area by a fixed amount without providing a view
|
|
781
|
+
|
|
782
|
+
```swift
|
|
783
|
+
// Add padding to safe area
|
|
784
|
+
ScrollView {
|
|
785
|
+
content
|
|
786
|
+
}
|
|
787
|
+
.safeAreaPadding(.horizontal, 20)
|
|
788
|
+
.safeAreaPadding(.bottom, 60)
|
|
789
|
+
|
|
790
|
+
// Equivalent to safeAreaInset but cleaner when you don't need a view
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
**Difference from `.padding()`:**
|
|
794
|
+
- `.padding()` adds space but doesn't affect safe area calculations
|
|
795
|
+
- `.safeAreaPadding()` extends the safe area itself
|
|
796
|
+
|
|
797
|
+
### Accessing safe area values
|
|
798
|
+
|
|
799
|
+
```swift
|
|
800
|
+
// Read safe area insets
|
|
801
|
+
GeometryReader { geometry in
|
|
802
|
+
let safeArea = geometry.safeAreaInsets
|
|
803
|
+
|
|
804
|
+
VStack {
|
|
805
|
+
Text("Top: \(safeArea.top)")
|
|
806
|
+
Text("Bottom: \(safeArea.bottom)")
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
**See also:** [Managing safe area in SwiftUI](https://swiftwithmajid.com/2021/11/03/managing-safe-area-in-swiftui/)
|
|
812
|
+
</safe_areas>
|
|
813
|
+
|
|
814
|
+
<decision_tree>
|
|
815
|
+
## Choosing the Right Layout
|
|
816
|
+
|
|
817
|
+
**Simple horizontal arrangement:**
|
|
818
|
+
- Use `HStack` with alignment and spacing parameters
|
|
819
|
+
- Use `Spacer()` to push content to edges
|
|
820
|
+
|
|
821
|
+
**Simple vertical arrangement:**
|
|
822
|
+
- Use `VStack` with alignment and spacing parameters
|
|
823
|
+
- Consider `LazyVStack` if list exceeds 50+ items
|
|
824
|
+
|
|
825
|
+
**Overlapping views:**
|
|
826
|
+
- Use `ZStack` for basic layering
|
|
827
|
+
- Use `.overlay()` or `.background()` for single overlay/underlay
|
|
828
|
+
- Consider Custom Layout for complex Z-ordering logic
|
|
829
|
+
|
|
830
|
+
**Long scrolling list:**
|
|
831
|
+
- Use `LazyVStack` inside `ScrollView` for variable content
|
|
832
|
+
- Use `List` for standard iOS list appearance with built-in features (swipe actions, separators)
|
|
833
|
+
- Vertical scrolling: `ScrollView { LazyVStack { } }`
|
|
834
|
+
- Horizontal scrolling: `ScrollView(.horizontal) { LazyHStack { } }`
|
|
835
|
+
|
|
836
|
+
**Grid of items:**
|
|
837
|
+
- **Small grid (< 20 items):** Use `Grid` for full control
|
|
838
|
+
- **Large grid:** Use `LazyVGrid` or `LazyHGrid`
|
|
839
|
+
- **Fixed columns:** `LazyVGrid(columns: [GridItem(.flexible()), ...])`
|
|
840
|
+
- **Adaptive columns:** `LazyVGrid(columns: [GridItem(.adaptive(minimum: 120))])`
|
|
841
|
+
|
|
842
|
+
**Need parent size:**
|
|
843
|
+
- **iOS 17+:** Use `containerRelativeFrame()` for size relative to container
|
|
844
|
+
- **iOS 16+:** Use `ViewThatFits` for adaptive layouts
|
|
845
|
+
- **Custom drawing/gestures:** Use `GeometryReader` sparingly
|
|
846
|
+
- **Simple fills:** Use `.frame(maxWidth: .infinity)`
|
|
847
|
+
|
|
848
|
+
**Adaptive layout (changes based on space):**
|
|
849
|
+
- Use `ViewThatFits` (iOS 16+) to switch between layouts
|
|
850
|
+
- Use size classes with `@Environment(\.horizontalSizeClass)`
|
|
851
|
+
|
|
852
|
+
**Complex custom layout:**
|
|
853
|
+
- Implement Custom Layout protocol (iOS 16+)
|
|
854
|
+
- Use for: flow layouts, radial layouts, masonry grids
|
|
855
|
+
- Provides full control over sizing and positioning
|
|
856
|
+
|
|
857
|
+
**Performance considerations:**
|
|
858
|
+
|
|
859
|
+
| Scenario | Recommendation | Reason |
|
|
860
|
+
|----------|---------------|--------|
|
|
861
|
+
| Static grid < 20 items | Grid | Simpler, all layout upfront |
|
|
862
|
+
| Dynamic list 50+ items | LazyVStack | Only renders visible |
|
|
863
|
+
| Photo gallery 100+ items | LazyVGrid | Memory efficient |
|
|
864
|
+
| Constantly changing list | LazyVStack with `.id()` | Controls view identity |
|
|
865
|
+
| macOS high FPS requirement | UIKit/AppKit wrapper | SwiftUI grids cap at ~90fps |
|
|
866
|
+
| Complex nesting 5+ levels | Custom Layout | Better control, fewer containers |
|
|
867
|
+
|
|
868
|
+
**See also:** `performance.md` for detailed performance tuning strategies
|
|
869
|
+
</decision_tree>
|
|
870
|
+
|
|
871
|
+
<anti_patterns>
|
|
872
|
+
## What NOT to Do
|
|
873
|
+
|
|
874
|
+
<anti_pattern name="GeometryReader for everything">
|
|
875
|
+
**Problem:** Using GeometryReader when simpler solutions exist
|
|
876
|
+
|
|
877
|
+
**Example:**
|
|
878
|
+
```swift
|
|
879
|
+
// ❌ Overcomplicated
|
|
880
|
+
GeometryReader { geometry in
|
|
881
|
+
Rectangle()
|
|
882
|
+
.frame(width: geometry.size.width * 0.8)
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// ✅ Simple and correct
|
|
886
|
+
Rectangle()
|
|
887
|
+
.frame(maxWidth: .infinity)
|
|
888
|
+
.padding(.horizontal, 40) // Creates inset
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
**Why it's bad:**
|
|
892
|
+
- GeometryReader expands to fill all space, breaking layouts
|
|
893
|
+
- Causes performance overhead
|
|
894
|
+
- Makes code harder to understand
|
|
895
|
+
- Often causes issues in ScrollViews
|
|
896
|
+
|
|
897
|
+
**Instead:**
|
|
898
|
+
- Use `.frame(maxWidth: .infinity)` for full width
|
|
899
|
+
- Use `containerRelativeFrame()` (iOS 17+) for proportional sizing
|
|
900
|
+
- Use `ViewThatFits` (iOS 16+) for adaptive layouts
|
|
901
|
+
- Reserve GeometryReader for actual coordinate-space needs
|
|
902
|
+
</anti_pattern>
|
|
903
|
+
|
|
904
|
+
<anti_pattern name="Nested Stacks explosion">
|
|
905
|
+
**Problem:** Excessive nesting of HStack/VStack creating deep hierarchies
|
|
906
|
+
|
|
907
|
+
**Example:**
|
|
908
|
+
```swift
|
|
909
|
+
// ❌ Too many nested stacks
|
|
910
|
+
VStack {
|
|
911
|
+
HStack {
|
|
912
|
+
VStack {
|
|
913
|
+
HStack {
|
|
914
|
+
VStack {
|
|
915
|
+
Text("Title")
|
|
916
|
+
Text("Subtitle")
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// ✅ Flattened with proper modifiers
|
|
924
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
925
|
+
Text("Title")
|
|
926
|
+
.font(.headline)
|
|
927
|
+
Text("Subtitle")
|
|
928
|
+
.font(.subheadline)
|
|
929
|
+
.foregroundStyle(.secondary)
|
|
930
|
+
}
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
**Why it's bad:**
|
|
934
|
+
- Harder to read and maintain
|
|
935
|
+
- Unnecessary view hierarchy depth
|
|
936
|
+
- Can impact performance with many views
|
|
937
|
+
- Makes alignment more complex
|
|
938
|
+
|
|
939
|
+
**Instead:**
|
|
940
|
+
- Use alignment and spacing parameters instead of wrapper stacks
|
|
941
|
+
- Extract complex views into separate components
|
|
942
|
+
- Use Grid for form-like layouts
|
|
943
|
+
- Consider Custom Layout for truly complex arrangements
|
|
944
|
+
</anti_pattern>
|
|
945
|
+
|
|
946
|
+
<anti_pattern name="LazyVStack without ScrollView">
|
|
947
|
+
**Problem:** Using LazyVStack outside a ScrollView
|
|
948
|
+
|
|
949
|
+
**Example:**
|
|
950
|
+
```swift
|
|
951
|
+
// ❌ LazyVStack needs a scrollable container
|
|
952
|
+
LazyVStack {
|
|
953
|
+
ForEach(items) { item in
|
|
954
|
+
Text(item.name)
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// ✅ Correct usage
|
|
959
|
+
ScrollView {
|
|
960
|
+
LazyVStack {
|
|
961
|
+
ForEach(items) { item in
|
|
962
|
+
Text(item.name)
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// ✅ Or just use VStack if not scrolling
|
|
968
|
+
VStack {
|
|
969
|
+
ForEach(items) { item in
|
|
970
|
+
Text(item.name)
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
**Why it's bad:**
|
|
976
|
+
- LazyVStack requires a scrollable parent to know when to load views
|
|
977
|
+
- Without scrolling, there's no benefit to lazy loading
|
|
978
|
+
- Can cause unexpected layout behavior
|
|
979
|
+
|
|
980
|
+
**Instead:**
|
|
981
|
+
- Always wrap LazyVStack/LazyHStack in ScrollView
|
|
982
|
+
- If not scrolling, use regular VStack/HStack
|
|
983
|
+
</anti_pattern>
|
|
984
|
+
|
|
985
|
+
<anti_pattern name="Fixed GridItem sizes everywhere">
|
|
986
|
+
**Problem:** Using `.fixed()` GridItem when flexible sizing would work better
|
|
987
|
+
|
|
988
|
+
**Example:**
|
|
989
|
+
```swift
|
|
990
|
+
// ❌ Fixed sizes break on different screen sizes
|
|
991
|
+
LazyVGrid(columns: [
|
|
992
|
+
GridItem(.fixed(150)),
|
|
993
|
+
GridItem(.fixed(150))
|
|
994
|
+
]) {
|
|
995
|
+
ForEach(items) { item in
|
|
996
|
+
ItemView(item: item)
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// ✅ Adaptive sizing
|
|
1001
|
+
LazyVGrid(columns: [
|
|
1002
|
+
GridItem(.adaptive(minimum: 150, maximum: 200))
|
|
1003
|
+
]) {
|
|
1004
|
+
ForEach(items) { item in
|
|
1005
|
+
ItemView(item: item)
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// ✅ Flexible columns
|
|
1010
|
+
LazyVGrid(columns: [
|
|
1011
|
+
GridItem(.flexible()),
|
|
1012
|
+
GridItem(.flexible())
|
|
1013
|
+
]) {
|
|
1014
|
+
ForEach(items) { item in
|
|
1015
|
+
ItemView(item: item)
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
**Why it's bad:**
|
|
1021
|
+
- Doesn't adapt to different screen sizes (iPhone SE vs iPad)
|
|
1022
|
+
- Creates horizontal scrolling or cut-off content
|
|
1023
|
+
- Not responsive to orientation changes
|
|
1024
|
+
|
|
1025
|
+
**Instead:**
|
|
1026
|
+
- Use `.flexible()` to let items share space proportionally
|
|
1027
|
+
- Use `.adaptive()` to fit as many items as possible
|
|
1028
|
+
- Reserve `.fixed()` for specific design requirements (icons, avatars)
|
|
1029
|
+
</anti_pattern>
|
|
1030
|
+
|
|
1031
|
+
<anti_pattern name="Spacer() abuse">
|
|
1032
|
+
**Problem:** Using multiple Spacers when alignment parameters would be clearer
|
|
1033
|
+
|
|
1034
|
+
**Example:**
|
|
1035
|
+
```swift
|
|
1036
|
+
// ❌ Confusing spacer usage
|
|
1037
|
+
HStack {
|
|
1038
|
+
Spacer()
|
|
1039
|
+
Text("Centered?")
|
|
1040
|
+
Spacer()
|
|
1041
|
+
Spacer()
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// ✅ Clear alignment
|
|
1045
|
+
HStack {
|
|
1046
|
+
Spacer()
|
|
1047
|
+
Text("Centered")
|
|
1048
|
+
Spacer()
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// ✅ Even better - use alignment
|
|
1052
|
+
HStack {
|
|
1053
|
+
Text("Centered")
|
|
1054
|
+
}
|
|
1055
|
+
.frame(maxWidth: .infinity)
|
|
1056
|
+
|
|
1057
|
+
// ✅ For trailing alignment
|
|
1058
|
+
HStack {
|
|
1059
|
+
Spacer()
|
|
1060
|
+
Text("Trailing")
|
|
1061
|
+
}
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
**Why it's bad:**
|
|
1065
|
+
- Multiple spacers create ambiguous spacing
|
|
1066
|
+
- Harder to reason about layout
|
|
1067
|
+
- Can cause unexpected behavior with different content sizes
|
|
1068
|
+
|
|
1069
|
+
**Instead:**
|
|
1070
|
+
- Use single Spacer() for clear intent
|
|
1071
|
+
- Use frame modifiers with alignment
|
|
1072
|
+
- Use stack alignment parameters
|
|
1073
|
+
</anti_pattern>
|
|
1074
|
+
|
|
1075
|
+
<anti_pattern name="Mixing lazy and non-lazy inappropriately">
|
|
1076
|
+
**Problem:** Using LazyVStack for small lists or VStack for huge lists
|
|
1077
|
+
|
|
1078
|
+
**Example:**
|
|
1079
|
+
```swift
|
|
1080
|
+
// ❌ Lazy overhead for tiny list
|
|
1081
|
+
ScrollView {
|
|
1082
|
+
LazyVStack {
|
|
1083
|
+
ForEach(0..<5) { i in
|
|
1084
|
+
Text("Item \(i)")
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// ✅ Just use VStack
|
|
1090
|
+
ScrollView {
|
|
1091
|
+
VStack {
|
|
1092
|
+
ForEach(0..<5) { i in
|
|
1093
|
+
Text("Item \(i)")
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// ❌ Regular stack for huge list
|
|
1099
|
+
VStack {
|
|
1100
|
+
ForEach(0..<1000) { i in
|
|
1101
|
+
ExpensiveView(index: i)
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// ✅ Lazy for performance
|
|
1106
|
+
ScrollView {
|
|
1107
|
+
LazyVStack {
|
|
1108
|
+
ForEach(0..<1000) { i in
|
|
1109
|
+
ExpensiveView(index: i)
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
**Why it's bad:**
|
|
1116
|
+
- Lazy containers add overhead for small datasets
|
|
1117
|
+
- Non-lazy containers create all views upfront (memory/performance hit)
|
|
1118
|
+
|
|
1119
|
+
**Instead:**
|
|
1120
|
+
- **< 20 simple items:** Use VStack/HStack
|
|
1121
|
+
- **20-50 items:** Test both; likely VStack is fine
|
|
1122
|
+
- **> 50 items or complex views:** Use LazyVStack/LazyHStack
|
|
1123
|
+
- **Large images/media:** Always use lazy
|
|
1124
|
+
</anti_pattern>
|
|
1125
|
+
|
|
1126
|
+
<anti_pattern name="ViewThatFits with fixed frames">
|
|
1127
|
+
**Problem:** Providing fixed frames to ViewThatFits children, defeating its purpose
|
|
1128
|
+
|
|
1129
|
+
**Example:**
|
|
1130
|
+
```swift
|
|
1131
|
+
// ❌ Fixed frames prevent ViewThatFits from working
|
|
1132
|
+
ViewThatFits {
|
|
1133
|
+
HStack {
|
|
1134
|
+
content
|
|
1135
|
+
}
|
|
1136
|
+
.frame(width: 600) // Prevents fitting logic
|
|
1137
|
+
|
|
1138
|
+
VStack {
|
|
1139
|
+
content
|
|
1140
|
+
}
|
|
1141
|
+
.frame(width: 300)
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// ✅ Let views size naturally
|
|
1145
|
+
ViewThatFits {
|
|
1146
|
+
HStack {
|
|
1147
|
+
content
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
VStack {
|
|
1151
|
+
content
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
**Why it's bad:**
|
|
1157
|
+
- ViewThatFits needs to measure ideal sizes to choose the right view
|
|
1158
|
+
- Fixed frames override this measurement
|
|
1159
|
+
- Defeats the entire purpose of adaptive layout
|
|
1160
|
+
|
|
1161
|
+
**Instead:**
|
|
1162
|
+
- Let child views size themselves naturally
|
|
1163
|
+
- Use maxWidth/maxHeight if needed, not fixed sizes
|
|
1164
|
+
- Trust ViewThatFits to pick the right layout
|
|
1165
|
+
</anti_pattern>
|
|
1166
|
+
|
|
1167
|
+
</anti_patterns>
|
|
1168
|
+
|
|
1169
|
+
**Sources:**
|
|
1170
|
+
Research for this reference included:
|
|
1171
|
+
- [SwiftUI Layout System (kean.blog)](https://kean.blog/post/swiftui-layout-system)
|
|
1172
|
+
- [Custom Layouts in SwiftUI (Medium)](https://medium.com/@wesleymatlock/custom-layouts-in-swiftui-a-deep-dive-into-the-layout-protocol-5edc691cd4fb)
|
|
1173
|
+
- [A guide to the SwiftUI layout system (Swift by Sundell)](https://www.swiftbysundell.com/articles/swiftui-layout-system-guide-part-1/)
|
|
1174
|
+
- [Creating custom layouts with Layout protocol (Hacking with Swift)](https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-a-custom-layout-using-the-layout-protocol)
|
|
1175
|
+
- [Apple Developer: Composing custom layouts with SwiftUI](https://developer.apple.com/documentation/swiftui/composing_custom_layouts_with_swiftui)
|
|
1176
|
+
- [Custom Layout in SwiftUI (Sarunw)](https://sarunw.com/posts/swiftui-custom-layout/)
|
|
1177
|
+
- [GeometryReader - Blessing or Curse? (fatbobman)](https://fatbobman.com/en/posts/geometryreader-blessing-or-curse/)
|
|
1178
|
+
- [Mastering GeometryReader in SwiftUI (DEV Community)](https://dev.to/qmshahzad/mastering-geometryreader-in-swiftui-from-basics-to-advanced-layout-control-5akk)
|
|
1179
|
+
- [SwiftUI Grid, LazyVGrid, LazyHGrid (avanderlee)](https://www.avanderlee.com/swiftui/grid-lazyvgrid-lazyhgrid-gridviews/)
|
|
1180
|
+
- [Tuning Lazy Stacks and Grids Performance Guide (Medium)](https://medium.com/@wesleymatlock/tuning-lazy-stacks-and-grids-in-swiftui-a-performance-guide-2fb10786f76a)
|
|
1181
|
+
- [containerRelativeFrame Modifier (fatbobman)](https://fatbobman.com/en/posts/mastering-the-containerrelativeframe-modifier-in-swiftui/)
|
|
1182
|
+
- [ViewThatFits adaptive layout (Hacking with Swift)](https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-an-adaptive-layout-with-viewthatfits)
|
|
1183
|
+
- [Mastering ViewThatFits (fatbobman)](https://fatbobman.com/en/posts/mastering-viewthatfits/)
|
|
1184
|
+
- [Alignment Guides in SwiftUI (The SwiftUI Lab)](https://swiftui-lab.com/alignment-guides/)
|
|
1185
|
+
- [Managing safe area in SwiftUI (Swift with Majid)](https://swiftwithmajid.com/2021/11/03/managing-safe-area-in-swiftui/)
|
|
1186
|
+
- [Mastering Safe Area in SwiftUI (fatbobman)](https://fatbobman.com/en/posts/safearea/)
|