claudecode-omc 5.6.6 → 5.6.8
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/.local/skills/THIRD_PARTY_LICENSES/AvdLee-SwiftUI-Agent-Skill.LICENSE +21 -0
- package/.local/skills/THIRD_PARTY_LICENSES/Dimillian-Skills.LICENSE +21 -0
- package/.local/skills/THIRD_PARTY_LICENSES/README.md +36 -0
- package/.local/skills/THIRD_PARTY_LICENSES/twostraws-swiftui-agent-skill.LICENSE +21 -0
- package/.local/skills/h5-to-swiftui/SKILL.md +201 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/README.md +176 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/index.html +52 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/style.css +133 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Package.swift +26 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Sources/CalibrationScreen/CalibrationScreen.swift +142 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Package.swift +32 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Sources/CalibrationScreenDivergent/CalibrationScreenDivergent.swift +122 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/tokens.json +42 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/index.html +14 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/package.json +20 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/001.json +96 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/index.json +89 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.jsx +22 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.module.css +11 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.jsx +53 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.module.css +139 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.jsx +37 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.module.css +72 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.jsx +30 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.module.css +50 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.jsx +159 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.module.css +21 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/main.jsx +12 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.jsx +182 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.module.css +294 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.jsx +147 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.module.css +161 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/global.css +50 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/tokens.css +103 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/vite.config.js +6 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/data/tasks.js +67 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/index.html +26 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/router.js +73 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/detail.js +164 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/home.js +53 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/list.js +87 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/app.css +342 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/tokens.css +68 -0
- package/.local/skills/h5-to-swiftui/references/css-to-swiftui-map.md +205 -0
- package/.local/skills/h5-to-swiftui/references/design-token-extraction.md +209 -0
- package/.local/skills/h5-to-swiftui/references/high-risk-triage.md +209 -0
- package/.local/skills/h5-to-swiftui/references/render-equivalence-calibration.md +193 -0
- package/.local/skills/h5-to-swiftui/references/stack-detection.md +160 -0
- package/.local/skills/h5-to-swiftui/references/visual-diff-loop-protocol.md +365 -0
- package/.local/skills/h5-to-swiftui/scripts/_calib-consts.mjs +150 -0
- package/.local/skills/h5-to-swiftui/scripts/_imglib.mjs +547 -0
- package/.local/skills/h5-to-swiftui/scripts/_provenance.mjs +123 -0
- package/.local/skills/h5-to-swiftui/scripts/calibrate-render.mjs +625 -0
- package/.local/skills/h5-to-swiftui/scripts/capture-reference.mjs +386 -0
- package/.local/skills/h5-to-swiftui/scripts/detect-stack.mjs +305 -0
- package/.local/skills/h5-to-swiftui/scripts/evaluate-convergence.mjs +1093 -0
- package/.local/skills/h5-to-swiftui/scripts/extract-tokens.mjs +600 -0
- package/.local/skills/h5-to-swiftui/scripts/mark-overlay.mjs +379 -0
- package/.local/skills/h5-to-swiftui/scripts/pixel-diff.mjs +530 -0
- package/.local/skills/h5-to-swiftui/scripts/sim-screenshot.sh +544 -0
- package/.local/skills/ios-debugger-agent/SKILL.md +51 -0
- package/.local/skills/ios-debugger-agent/agents/openai.yaml +4 -0
- package/.local/skills/swift-concurrency-expert/SKILL.md +105 -0
- package/.local/skills/swift-concurrency-expert/agents/openai.yaml +4 -0
- package/.local/skills/swift-concurrency-expert/references/approachable-concurrency.md +63 -0
- package/.local/skills/swift-concurrency-expert/references/swift-6-2-concurrency.md +272 -0
- package/.local/skills/swift-concurrency-expert/references/swiftui-concurrency-tour-wwdc.md +33 -0
- package/.local/skills/swiftui-expert-skill/SKILL.md +162 -0
- package/.local/skills/swiftui-expert-skill/references/accessibility-patterns.md +215 -0
- package/.local/skills/swiftui-expert-skill/references/animation-advanced.md +403 -0
- package/.local/skills/swiftui-expert-skill/references/animation-basics.md +284 -0
- package/.local/skills/swiftui-expert-skill/references/animation-transitions.md +326 -0
- package/.local/skills/swiftui-expert-skill/references/charts-accessibility.md +135 -0
- package/.local/skills/swiftui-expert-skill/references/charts.md +602 -0
- package/.local/skills/swiftui-expert-skill/references/focus-patterns.md +299 -0
- package/.local/skills/swiftui-expert-skill/references/image-optimization.md +203 -0
- package/.local/skills/swiftui-expert-skill/references/latest-apis.md +488 -0
- package/.local/skills/swiftui-expert-skill/references/layout-best-practices.md +266 -0
- package/.local/skills/swiftui-expert-skill/references/liquid-glass.md +423 -0
- package/.local/skills/swiftui-expert-skill/references/list-patterns.md +446 -0
- package/.local/skills/swiftui-expert-skill/references/macos-scenes.md +318 -0
- package/.local/skills/swiftui-expert-skill/references/macos-views.md +357 -0
- package/.local/skills/swiftui-expert-skill/references/macos-window-styling.md +303 -0
- package/.local/skills/swiftui-expert-skill/references/performance-patterns.md +403 -0
- package/.local/skills/swiftui-expert-skill/references/scroll-patterns.md +293 -0
- package/.local/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md +363 -0
- package/.local/skills/swiftui-expert-skill/references/state-management.md +388 -0
- package/.local/skills/swiftui-expert-skill/references/text-patterns.md +32 -0
- package/.local/skills/swiftui-expert-skill/references/trace-analysis.md +295 -0
- package/.local/skills/swiftui-expert-skill/references/trace-recording.md +134 -0
- package/.local/skills/swiftui-expert-skill/references/view-structure.md +780 -0
- package/.local/skills/swiftui-expert-skill/scripts/__pycache__/analyze_trace.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/__pycache__/record_trace.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/analyze_trace.py +301 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__init__.py +1 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/__init__.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/causes.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/correlate.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/events.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hangs.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hitches.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/summary.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/swiftui.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/time_profiler.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xctrace.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xml_utils.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/causes.py +187 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/correlate.py +179 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/events.py +291 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hangs.py +108 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hitches.py +145 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/summary.py +243 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/swiftui.py +195 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/time_profiler.py +135 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py +117 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py +224 -0
- package/.local/skills/swiftui-expert-skill/scripts/record_trace.py +252 -0
- package/.local/skills/swiftui-liquid-glass/SKILL.md +90 -0
- package/.local/skills/swiftui-liquid-glass/agents/openai.yaml +4 -0
- package/.local/skills/swiftui-liquid-glass/references/liquid-glass.md +280 -0
- package/.local/skills/swiftui-performance-audit/SKILL.md +106 -0
- package/.local/skills/swiftui-performance-audit/agents/openai.yaml +4 -0
- package/.local/skills/swiftui-performance-audit/references/code-smells.md +150 -0
- package/.local/skills/swiftui-performance-audit/references/demystify-swiftui-performance-wwdc23.md +46 -0
- package/.local/skills/swiftui-performance-audit/references/optimizing-swiftui-performance-instruments.md +29 -0
- package/.local/skills/swiftui-performance-audit/references/profiling-intake.md +44 -0
- package/.local/skills/swiftui-performance-audit/references/report-template.md +47 -0
- package/.local/skills/swiftui-performance-audit/references/understanding-hangs-in-your-app.md +33 -0
- package/.local/skills/swiftui-performance-audit/references/understanding-improving-swiftui-performance.md +52 -0
- package/.local/skills/swiftui-pro/SKILL.md +108 -0
- package/.local/skills/swiftui-pro/agents/openai.yaml +10 -0
- package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.png +0 -0
- package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.svg +29 -0
- package/.local/skills/swiftui-pro/references/accessibility.md +13 -0
- package/.local/skills/swiftui-pro/references/api.md +39 -0
- package/.local/skills/swiftui-pro/references/data.md +43 -0
- package/.local/skills/swiftui-pro/references/design.md +32 -0
- package/.local/skills/swiftui-pro/references/hygiene.md +9 -0
- package/.local/skills/swiftui-pro/references/navigation.md +14 -0
- package/.local/skills/swiftui-pro/references/performance.md +46 -0
- package/.local/skills/swiftui-pro/references/swift.md +56 -0
- package/.local/skills/swiftui-pro/references/views.md +36 -0
- package/.local/skills/swiftui-ui-patterns/SKILL.md +95 -0
- package/.local/skills/swiftui-ui-patterns/agents/openai.yaml +4 -0
- package/.local/skills/swiftui-ui-patterns/references/app-wiring.md +201 -0
- package/.local/skills/swiftui-ui-patterns/references/async-state.md +96 -0
- package/.local/skills/swiftui-ui-patterns/references/components-index.md +50 -0
- package/.local/skills/swiftui-ui-patterns/references/controls.md +57 -0
- package/.local/skills/swiftui-ui-patterns/references/deeplinks.md +66 -0
- package/.local/skills/swiftui-ui-patterns/references/focus.md +90 -0
- package/.local/skills/swiftui-ui-patterns/references/form.md +97 -0
- package/.local/skills/swiftui-ui-patterns/references/grids.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/haptics.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/input-toolbar.md +51 -0
- package/.local/skills/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
- package/.local/skills/swiftui-ui-patterns/references/list.md +86 -0
- package/.local/skills/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
- package/.local/skills/swiftui-ui-patterns/references/macos-settings.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/matched-transitions.md +59 -0
- package/.local/skills/swiftui-ui-patterns/references/media.md +73 -0
- package/.local/skills/swiftui-ui-patterns/references/menu-bar.md +101 -0
- package/.local/skills/swiftui-ui-patterns/references/navigationstack.md +159 -0
- package/.local/skills/swiftui-ui-patterns/references/overlay.md +45 -0
- package/.local/skills/swiftui-ui-patterns/references/performance.md +62 -0
- package/.local/skills/swiftui-ui-patterns/references/previews.md +48 -0
- package/.local/skills/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
- package/.local/skills/swiftui-ui-patterns/references/scrollview.md +87 -0
- package/.local/skills/swiftui-ui-patterns/references/searchable.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/sheets.md +155 -0
- package/.local/skills/swiftui-ui-patterns/references/split-views.md +72 -0
- package/.local/skills/swiftui-ui-patterns/references/tabview.md +114 -0
- package/.local/skills/swiftui-ui-patterns/references/theming.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/title-menus.md +93 -0
- package/.local/skills/swiftui-ui-patterns/references/top-bar.md +49 -0
- package/.local/skills/swiftui-view-refactor/SKILL.md +202 -0
- package/.local/skills/swiftui-view-refactor/agents/openai.yaml +4 -0
- package/.local/skills/swiftui-view-refactor/references/mv-patterns.md +161 -0
- package/bundled/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# SwiftUI ScrollView Patterns Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [ScrollViewReader for Programmatic Scrolling](#scrollviewreader-for-programmatic-scrolling)
|
|
6
|
+
- [Scroll Position Tracking](#scroll-position-tracking)
|
|
7
|
+
- [Scroll Transitions and Effects](#scroll-transitions-and-effects)
|
|
8
|
+
- [Scroll Target Behavior](#scroll-target-behavior)
|
|
9
|
+
- [Summary Checklist](#summary-checklist)
|
|
10
|
+
|
|
11
|
+
## ScrollViewReader for Programmatic Scrolling
|
|
12
|
+
|
|
13
|
+
**Use `ScrollViewReader` for scroll-to-top, scroll-to-bottom, and anchor-based jumps.**
|
|
14
|
+
|
|
15
|
+
```swift
|
|
16
|
+
struct ChatView: View {
|
|
17
|
+
@State private var messages: [Message] = []
|
|
18
|
+
private let bottomID = "bottom"
|
|
19
|
+
|
|
20
|
+
var body: some View {
|
|
21
|
+
ScrollViewReader { proxy in
|
|
22
|
+
ScrollView {
|
|
23
|
+
LazyVStack {
|
|
24
|
+
ForEach(messages) { message in
|
|
25
|
+
MessageRow(message: message)
|
|
26
|
+
.id(message.id)
|
|
27
|
+
}
|
|
28
|
+
Color.clear
|
|
29
|
+
.frame(height: 1)
|
|
30
|
+
.id(bottomID)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
.onChange(of: messages.count) { _, _ in
|
|
34
|
+
withAnimation {
|
|
35
|
+
proxy.scrollTo(bottomID, anchor: .bottom)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
.onAppear {
|
|
39
|
+
proxy.scrollTo(bottomID, anchor: .bottom)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Scroll-to-Top Pattern
|
|
47
|
+
|
|
48
|
+
```swift
|
|
49
|
+
struct FeedView: View {
|
|
50
|
+
@State private var items: [Item] = []
|
|
51
|
+
@State private var scrollToTop = false
|
|
52
|
+
private let topID = "top"
|
|
53
|
+
|
|
54
|
+
var body: some View {
|
|
55
|
+
ScrollViewReader { proxy in
|
|
56
|
+
ScrollView {
|
|
57
|
+
LazyVStack {
|
|
58
|
+
Color.clear
|
|
59
|
+
.frame(height: 1)
|
|
60
|
+
.id(topID)
|
|
61
|
+
|
|
62
|
+
ForEach(items) { item in
|
|
63
|
+
ItemRow(item: item)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
.onChange(of: scrollToTop) { _, shouldScroll in
|
|
68
|
+
if shouldScroll {
|
|
69
|
+
withAnimation {
|
|
70
|
+
proxy.scrollTo(topID, anchor: .top)
|
|
71
|
+
}
|
|
72
|
+
scrollToTop = false
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Why**: `ScrollViewReader` provides programmatic scroll control with stable anchors. Always use stable IDs and explicit animations.
|
|
81
|
+
|
|
82
|
+
## Scroll Position Tracking
|
|
83
|
+
|
|
84
|
+
### Basic Scroll Position
|
|
85
|
+
|
|
86
|
+
**Avoid** - Storing scroll position directly triggers view updates on every scroll frame:
|
|
87
|
+
|
|
88
|
+
```swift
|
|
89
|
+
// ❌ Bad Practice - causes unnecessary re-renders
|
|
90
|
+
struct ContentView: View {
|
|
91
|
+
@State private var scrollPosition: CGFloat = 0
|
|
92
|
+
|
|
93
|
+
var body: some View {
|
|
94
|
+
ScrollView {
|
|
95
|
+
content
|
|
96
|
+
.background(
|
|
97
|
+
GeometryReader { geometry in
|
|
98
|
+
Color.clear
|
|
99
|
+
.preference(
|
|
100
|
+
key: ScrollOffsetPreferenceKey.self,
|
|
101
|
+
value: geometry.frame(in: .named("scroll")).minY
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
.coordinateSpace(name: "scroll")
|
|
107
|
+
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
|
|
108
|
+
scrollPosition = value
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Preferred** - Check scroll position and update a flag based on thresholds for smoother, more efficient scrolling:
|
|
115
|
+
|
|
116
|
+
```swift
|
|
117
|
+
// ✅ Good Practice - only updates state when crossing threshold
|
|
118
|
+
struct ContentView: View {
|
|
119
|
+
@State private var startAnimation: Bool = false
|
|
120
|
+
|
|
121
|
+
var body: some View {
|
|
122
|
+
ScrollView {
|
|
123
|
+
content
|
|
124
|
+
.background(
|
|
125
|
+
GeometryReader { geometry in
|
|
126
|
+
Color.clear
|
|
127
|
+
.preference(
|
|
128
|
+
key: ScrollOffsetPreferenceKey.self,
|
|
129
|
+
value: geometry.frame(in: .named("scroll")).minY
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
.coordinateSpace(name: "scroll")
|
|
135
|
+
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
|
|
136
|
+
if value < -100 {
|
|
137
|
+
startAnimation = true
|
|
138
|
+
} else {
|
|
139
|
+
startAnimation = false
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
struct ScrollOffsetPreferenceKey: PreferenceKey {
|
|
146
|
+
static var defaultValue: CGFloat = 0
|
|
147
|
+
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
|
148
|
+
value = nextValue()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Scroll-Based Header Visibility
|
|
154
|
+
|
|
155
|
+
```swift
|
|
156
|
+
struct ContentView: View {
|
|
157
|
+
@State private var showHeader = true
|
|
158
|
+
|
|
159
|
+
var body: some View {
|
|
160
|
+
VStack(spacing: 0) {
|
|
161
|
+
if showHeader {
|
|
162
|
+
HeaderView()
|
|
163
|
+
.transition(.move(edge: .top))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
ScrollView {
|
|
167
|
+
content
|
|
168
|
+
.background(
|
|
169
|
+
GeometryReader { geometry in
|
|
170
|
+
Color.clear
|
|
171
|
+
.preference(
|
|
172
|
+
key: ScrollOffsetPreferenceKey.self,
|
|
173
|
+
value: geometry.frame(in: .named("scroll")).minY
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
.coordinateSpace(name: "scroll")
|
|
179
|
+
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
|
|
180
|
+
if offset < -50 { // Scrolling down
|
|
181
|
+
withAnimation { showHeader = false }
|
|
182
|
+
} else if offset > 50 { // Scrolling up
|
|
183
|
+
withAnimation { showHeader = true }
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Scroll Transitions and Effects
|
|
192
|
+
|
|
193
|
+
> **iOS 17+**: All APIs in this section require iOS 17 or later.
|
|
194
|
+
|
|
195
|
+
### Scroll-Based Opacity
|
|
196
|
+
|
|
197
|
+
```swift
|
|
198
|
+
struct ParallaxView: View {
|
|
199
|
+
var body: some View {
|
|
200
|
+
ScrollView {
|
|
201
|
+
LazyVStack(spacing: 20) {
|
|
202
|
+
ForEach(items) { item in
|
|
203
|
+
ItemCard(item: item)
|
|
204
|
+
.visualEffect { content, geometry in
|
|
205
|
+
let frame = geometry.frame(in: .scrollView)
|
|
206
|
+
let distance = min(0, frame.minY)
|
|
207
|
+
return content
|
|
208
|
+
.opacity(1 + distance / 200)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Parallax Effect
|
|
218
|
+
|
|
219
|
+
```swift
|
|
220
|
+
struct ParallaxHeader: View {
|
|
221
|
+
var body: some View {
|
|
222
|
+
ScrollView {
|
|
223
|
+
VStack(spacing: 0) {
|
|
224
|
+
Image("hero")
|
|
225
|
+
.resizable()
|
|
226
|
+
.aspectRatio(contentMode: .fill)
|
|
227
|
+
.frame(height: 300)
|
|
228
|
+
.visualEffect { content, geometry in
|
|
229
|
+
let offset = geometry.frame(in: .scrollView).minY
|
|
230
|
+
return content
|
|
231
|
+
.offset(y: offset > 0 ? -offset * 0.5 : 0)
|
|
232
|
+
}
|
|
233
|
+
.clipped()
|
|
234
|
+
|
|
235
|
+
ContentView()
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Scroll Target Behavior
|
|
243
|
+
|
|
244
|
+
> **iOS 17+**: All APIs in this section require iOS 17 or later.
|
|
245
|
+
|
|
246
|
+
### Paging ScrollView
|
|
247
|
+
|
|
248
|
+
```swift
|
|
249
|
+
struct PagingView: View {
|
|
250
|
+
var body: some View {
|
|
251
|
+
ScrollView(.horizontal) {
|
|
252
|
+
LazyHStack(spacing: 0) {
|
|
253
|
+
ForEach(pages) { page in
|
|
254
|
+
PageView(page: page)
|
|
255
|
+
.containerRelativeFrame(.horizontal)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
.scrollTargetLayout()
|
|
259
|
+
}
|
|
260
|
+
.scrollTargetBehavior(.paging)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Snap to Items
|
|
266
|
+
|
|
267
|
+
```swift
|
|
268
|
+
struct SnapScrollView: View {
|
|
269
|
+
var body: some View {
|
|
270
|
+
ScrollView(.horizontal) {
|
|
271
|
+
LazyHStack(spacing: 16) {
|
|
272
|
+
ForEach(items) { item in
|
|
273
|
+
ItemCard(item: item)
|
|
274
|
+
.frame(width: 280)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
.scrollTargetLayout()
|
|
278
|
+
}
|
|
279
|
+
.scrollTargetBehavior(.viewAligned)
|
|
280
|
+
.contentMargins(.horizontal, 20)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Summary Checklist
|
|
286
|
+
|
|
287
|
+
- [ ] Use `ScrollViewReader` with stable IDs for programmatic scrolling
|
|
288
|
+
- [ ] Always use explicit animations with `scrollTo()`
|
|
289
|
+
- [ ] Use `.visualEffect` for scroll-based visual changes
|
|
290
|
+
- [ ] Use `.scrollTargetBehavior(.paging)` for paging behavior
|
|
291
|
+
- [ ] Use `.scrollTargetBehavior(.viewAligned)` for snap-to-item behavior
|
|
292
|
+
- [ ] Gate frequent scroll position updates by thresholds
|
|
293
|
+
- [ ] Use preference keys for custom scroll position tracking
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# SwiftUI Sheet, Navigation & Inspector Patterns Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [Sheet Patterns](#sheet-patterns)
|
|
6
|
+
- [Navigation Patterns](#navigation-patterns)
|
|
7
|
+
- [Multi-Column Navigation with NavigationSplitView](#multi-column-navigation-with-navigationsplitview)
|
|
8
|
+
- [Inspector](#inspector)
|
|
9
|
+
- [Presentation Modifiers](#presentation-modifiers)
|
|
10
|
+
- [Summary Checklist](#summary-checklist)
|
|
11
|
+
|
|
12
|
+
## Sheet Patterns
|
|
13
|
+
|
|
14
|
+
### Item-Driven Sheets (Preferred)
|
|
15
|
+
|
|
16
|
+
**Use `.sheet(item:)` instead of `.sheet(isPresented:)` when presenting model-based content.**
|
|
17
|
+
|
|
18
|
+
```swift
|
|
19
|
+
// Good - item-driven
|
|
20
|
+
@State private var selectedItem: Item?
|
|
21
|
+
|
|
22
|
+
var body: some View {
|
|
23
|
+
List(items) { item in
|
|
24
|
+
Button(item.name) {
|
|
25
|
+
selectedItem = item
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
.sheet(item: $selectedItem) { item in
|
|
29
|
+
ItemDetailSheet(item: item)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Avoid - boolean flag requires separate state
|
|
34
|
+
@State private var showSheet = false
|
|
35
|
+
@State private var selectedItem: Item?
|
|
36
|
+
|
|
37
|
+
var body: some View {
|
|
38
|
+
List(items) { item in
|
|
39
|
+
Button(item.name) {
|
|
40
|
+
selectedItem = item
|
|
41
|
+
showSheet = true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
.sheet(isPresented: $showSheet) {
|
|
45
|
+
if let selectedItem {
|
|
46
|
+
ItemDetailSheet(item: selectedItem)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Why**: `.sheet(item:)` automatically handles presentation state and avoids optional unwrapping in the sheet body.
|
|
53
|
+
|
|
54
|
+
### Sheets Own Their Actions
|
|
55
|
+
|
|
56
|
+
**Sheets should handle their own dismiss and actions internally** using `@Environment(\.dismiss)`. Avoid passing `onSave`/`onCancel` closures from the parent -- it creates callback prop-drilling and reduces reusability.
|
|
57
|
+
|
|
58
|
+
```swift
|
|
59
|
+
struct EditItemSheet: View {
|
|
60
|
+
@Environment(\.dismiss) private var dismiss
|
|
61
|
+
let item: Item
|
|
62
|
+
@State private var name: String
|
|
63
|
+
|
|
64
|
+
init(item: Item) {
|
|
65
|
+
self.item = item
|
|
66
|
+
_name = State(initialValue: item.name)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
var body: some View {
|
|
70
|
+
NavigationStack {
|
|
71
|
+
Form { TextField("Name", text: $name) }
|
|
72
|
+
.navigationTitle("Edit Item")
|
|
73
|
+
.toolbar {
|
|
74
|
+
ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } }
|
|
75
|
+
ToolbarItem(placement: .confirmationAction) { Button("Save") { /* save and dismiss */ } }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Enum-Based Sheet Management
|
|
83
|
+
|
|
84
|
+
When presenting multiple different sheets, use an `Identifiable` enum with `.sheet(item:)` instead of multiple boolean state properties:
|
|
85
|
+
|
|
86
|
+
```swift
|
|
87
|
+
struct ArticlesView: View {
|
|
88
|
+
enum Sheet: Identifiable {
|
|
89
|
+
case add, edit(Article), categories
|
|
90
|
+
var id: String {
|
|
91
|
+
switch self {
|
|
92
|
+
case .add: "add"
|
|
93
|
+
case .edit(let a): "edit-\(a.id)"
|
|
94
|
+
case .categories: "categories"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@State private var presentedSheet: Sheet?
|
|
100
|
+
|
|
101
|
+
var body: some View {
|
|
102
|
+
List { /* ... */ }
|
|
103
|
+
.toolbar {
|
|
104
|
+
Button("Add") { presentedSheet = .add }
|
|
105
|
+
}
|
|
106
|
+
.sheet(item: $presentedSheet) { sheet in
|
|
107
|
+
switch sheet {
|
|
108
|
+
case .add: AddArticleView()
|
|
109
|
+
case .edit(let article): EditArticleView(article: article)
|
|
110
|
+
case .categories: CategoriesView()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Why**: A single `@State` property and one `.sheet(item:)` modifier replaces N boolean properties and N sheet modifiers, improving readability and preventing only-one-sheet-at-a-time conflicts.
|
|
118
|
+
|
|
119
|
+
## Navigation Patterns
|
|
120
|
+
|
|
121
|
+
### Type-Safe Navigation with NavigationStack
|
|
122
|
+
|
|
123
|
+
```swift
|
|
124
|
+
struct ContentView: View {
|
|
125
|
+
var body: some View {
|
|
126
|
+
NavigationStack {
|
|
127
|
+
List {
|
|
128
|
+
NavigationLink("Profile", value: Route.profile)
|
|
129
|
+
NavigationLink("Settings", value: Route.settings)
|
|
130
|
+
}
|
|
131
|
+
.navigationDestination(for: Route.self) { route in
|
|
132
|
+
switch route {
|
|
133
|
+
case .profile:
|
|
134
|
+
ProfileView()
|
|
135
|
+
case .settings:
|
|
136
|
+
SettingsView()
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
enum Route: Hashable {
|
|
144
|
+
case profile
|
|
145
|
+
case settings
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Programmatic Navigation
|
|
150
|
+
|
|
151
|
+
```swift
|
|
152
|
+
struct ContentView: View {
|
|
153
|
+
@State private var navigationPath = NavigationPath()
|
|
154
|
+
|
|
155
|
+
var body: some View {
|
|
156
|
+
NavigationStack(path: $navigationPath) {
|
|
157
|
+
List {
|
|
158
|
+
Button("Go to Detail") {
|
|
159
|
+
navigationPath.append(DetailRoute.item(id: 1))
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
.navigationDestination(for: DetailRoute.self) { route in
|
|
163
|
+
switch route {
|
|
164
|
+
case .item(let id):
|
|
165
|
+
ItemDetailView(id: id)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
enum DetailRoute: Hashable {
|
|
173
|
+
case item(id: Int)
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Multi-Column Navigation with NavigationSplitView
|
|
178
|
+
|
|
179
|
+
### Two-Column Layout
|
|
180
|
+
|
|
181
|
+
Use `NavigationSplitView` for sidebar-driven navigation. Available on iOS 16+, macOS 13+, tvOS 16+, watchOS 9+.
|
|
182
|
+
|
|
183
|
+
```swift
|
|
184
|
+
struct ContentView: View {
|
|
185
|
+
@State private var selectedItem: Item.ID?
|
|
186
|
+
|
|
187
|
+
var body: some View {
|
|
188
|
+
NavigationSplitView {
|
|
189
|
+
List(items, selection: $selectedItem) { item in
|
|
190
|
+
Text(item.name)
|
|
191
|
+
}
|
|
192
|
+
.navigationTitle("Items")
|
|
193
|
+
} detail: {
|
|
194
|
+
if let selectedItem, let item = items.first(where: { $0.id == selectedItem }) {
|
|
195
|
+
ItemDetailView(item: item)
|
|
196
|
+
} else {
|
|
197
|
+
ContentUnavailableView("Select an Item", systemImage: "doc")
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Three-Column Layout
|
|
205
|
+
|
|
206
|
+
```swift
|
|
207
|
+
struct ContentView: View {
|
|
208
|
+
@State private var departmentId: Department.ID?
|
|
209
|
+
@State private var employeeIds = Set<Employee.ID>()
|
|
210
|
+
|
|
211
|
+
var body: some View {
|
|
212
|
+
NavigationSplitView {
|
|
213
|
+
List(model.departments, selection: $departmentId) { dept in
|
|
214
|
+
Text(dept.name)
|
|
215
|
+
}
|
|
216
|
+
} content: {
|
|
217
|
+
if let department = model.department(id: departmentId) {
|
|
218
|
+
List(department.employees, selection: $employeeIds) { emp in
|
|
219
|
+
Text(emp.name)
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
Text("Select a department")
|
|
223
|
+
}
|
|
224
|
+
} detail: {
|
|
225
|
+
EmployeeDetails(for: employeeIds)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Configuration
|
|
232
|
+
|
|
233
|
+
- **Column visibility**: `NavigationSplitView(columnVisibility: $visibility)` with `NavigationSplitViewVisibility` (`.detailOnly`, `.doubleColumn`, `.all`)
|
|
234
|
+
- **Column widths**: `.navigationSplitViewColumnWidth(min:ideal:max:)` on each column
|
|
235
|
+
- **Compact column**: `NavigationSplitView(preferredCompactColumn: $column)` to control which column shows on narrow devices
|
|
236
|
+
- **Style**: `.navigationSplitViewStyle(.balanced)` or `.prominentDetail` (default)
|
|
237
|
+
|
|
238
|
+
### Platform Behavior
|
|
239
|
+
|
|
240
|
+
| Platform | Behavior |
|
|
241
|
+
|----------|----------|
|
|
242
|
+
| **macOS** | Columns always visible side-by-side; sidebar has translucent material; variable-width column resizing by dragging |
|
|
243
|
+
| **iPadOS (regular)** | Sidebar can overlay or push detail; supports column visibility toggle via toolbar button |
|
|
244
|
+
| **iOS / iPadOS (compact)** | Collapses into a single `NavigationStack`; sidebar items show disclosure chevrons; back button navigates between columns |
|
|
245
|
+
| **iPhone (all sizes)** | Always collapsed into a stack; sidebar appears as the root list; selections push detail onto the stack |
|
|
246
|
+
| **watchOS / tvOS** | Collapses into a single stack |
|
|
247
|
+
|
|
248
|
+
## Inspector
|
|
249
|
+
|
|
250
|
+
> **Availability:** iOS 17.0+, macOS 14.0+
|
|
251
|
+
|
|
252
|
+
A trailing-edge panel for supplementary information.
|
|
253
|
+
|
|
254
|
+
On wider size classes (macOS, iPad landscape), it appears as a **trailing column**. On compact size classes (iPhone), it **adapts to a sheet** automatically.
|
|
255
|
+
|
|
256
|
+
### Basic Inspector
|
|
257
|
+
|
|
258
|
+
```swift
|
|
259
|
+
struct ShapeEditor: View {
|
|
260
|
+
@State private var showInspector = false
|
|
261
|
+
|
|
262
|
+
var body: some View {
|
|
263
|
+
MyEditorView()
|
|
264
|
+
.inspector(isPresented: $showInspector) {
|
|
265
|
+
InspectorContent()
|
|
266
|
+
}
|
|
267
|
+
.toolbar {
|
|
268
|
+
ToolbarItem {
|
|
269
|
+
Button {
|
|
270
|
+
showInspector.toggle()
|
|
271
|
+
} label: {
|
|
272
|
+
Label("Inspector", systemImage: "info.circle")
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Inspector with Column Width
|
|
281
|
+
|
|
282
|
+
```swift
|
|
283
|
+
MyEditorView()
|
|
284
|
+
.inspector(isPresented: $showInspector) {
|
|
285
|
+
InspectorContent()
|
|
286
|
+
.inspectorColumnWidth(min: 200, ideal: 250, max: 400)
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Inspector with Fixed Width
|
|
291
|
+
|
|
292
|
+
```swift
|
|
293
|
+
MyEditorView()
|
|
294
|
+
.inspector(isPresented: $showInspector) {
|
|
295
|
+
InspectorContent()
|
|
296
|
+
.inspectorColumnWidth(300)
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Platform Behavior
|
|
301
|
+
|
|
302
|
+
| Platform | Behavior |
|
|
303
|
+
|----------|----------|
|
|
304
|
+
| **macOS** | Trailing-edge sidebar panel; resizable by dragging edge; integrates with window toolbar |
|
|
305
|
+
| **iPadOS (regular)** | Trailing column alongside content; toggleable via toolbar button |
|
|
306
|
+
| **iOS / iPadOS (compact)** | Adapts to a sheet presentation; swipe-to-dismiss supported |
|
|
307
|
+
| **iPhone (all sizes)** | Always presented as a sheet (no trailing column); dismiss via swipe or button |
|
|
308
|
+
|
|
309
|
+
> **Tip:** Use `InspectorCommands` in your app's `.commands` to include the default inspector toggle keyboard shortcut.
|
|
310
|
+
|
|
311
|
+
## Presentation Modifiers
|
|
312
|
+
|
|
313
|
+
### Full Screen Cover
|
|
314
|
+
|
|
315
|
+
```swift
|
|
316
|
+
struct ContentView: View {
|
|
317
|
+
@State private var showFullScreen = false
|
|
318
|
+
|
|
319
|
+
var body: some View {
|
|
320
|
+
Button("Show Full Screen") {
|
|
321
|
+
showFullScreen = true
|
|
322
|
+
}
|
|
323
|
+
.fullScreenCover(isPresented: $showFullScreen) {
|
|
324
|
+
FullScreenView()
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Popover
|
|
331
|
+
|
|
332
|
+
```swift
|
|
333
|
+
struct ContentView: View {
|
|
334
|
+
@State private var showPopover = false
|
|
335
|
+
|
|
336
|
+
var body: some View {
|
|
337
|
+
Button("Show Popover") {
|
|
338
|
+
showPopover = true
|
|
339
|
+
}
|
|
340
|
+
.popover(isPresented: $showPopover) {
|
|
341
|
+
PopoverContentView()
|
|
342
|
+
.presentationCompactAdaptation(.popover) // Don't adapt to sheet on iPhone
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
For `alert` and `confirmationDialog` API patterns, see `latest-apis.md`.
|
|
349
|
+
|
|
350
|
+
## Summary Checklist
|
|
351
|
+
|
|
352
|
+
- [ ] Use `.sheet(item:)` for model-based sheets
|
|
353
|
+
- [ ] Sheets own their actions and dismiss internally
|
|
354
|
+
- [ ] Use `NavigationStack` with `navigationDestination(for:)` for type-safe navigation
|
|
355
|
+
- [ ] Use `NavigationPath` for programmatic navigation
|
|
356
|
+
- [ ] Use `NavigationSplitView` for sidebar-driven multi-column layouts
|
|
357
|
+
- [ ] Use `Inspector` for trailing-edge supplementary panels
|
|
358
|
+
- [ ] Set column widths with `navigationSplitViewColumnWidth(min:ideal:max:)` or `inspectorColumnWidth(min:ideal:max:)`
|
|
359
|
+
- [ ] Use appropriate presentation modifiers (sheet, fullScreenCover, popover)
|
|
360
|
+
- [ ] Alerts and confirmation dialogs use modern API with actions
|
|
361
|
+
- [ ] Avoid passing dismiss/save callbacks to sheets
|
|
362
|
+
- [ ] Use enum-based `Identifiable` type with `.sheet(item:)` when presenting multiple sheets
|
|
363
|
+
- [ ] Navigation state can be saved/restored when needed
|