claudecode-omc 5.6.7 → 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/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,299 @@
|
|
|
1
|
+
# SwiftUI Focus Patterns Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [@FocusState](#focusstate)
|
|
6
|
+
- [Making Views Focusable](#making-views-focusable)
|
|
7
|
+
- [Focused Values for Commands and Menus](#focused-values-for-commands-and-menus)
|
|
8
|
+
- [Default Focus](#default-focus)
|
|
9
|
+
- [Focus Scope and Sections](#focus-scope-and-sections)
|
|
10
|
+
- [Focus Effects](#focus-effects)
|
|
11
|
+
- [Search Focus](#search-focus)
|
|
12
|
+
- [Common Pitfalls](#common-pitfalls)
|
|
13
|
+
|
|
14
|
+
## @FocusState
|
|
15
|
+
|
|
16
|
+
Always mark `@FocusState` as `private`. Use `Bool` for a single field, an optional `Hashable` enum for multiple fields.
|
|
17
|
+
|
|
18
|
+
### Single field
|
|
19
|
+
|
|
20
|
+
```swift
|
|
21
|
+
@FocusState private var isFocused: Bool
|
|
22
|
+
|
|
23
|
+
TextField("Email", text: $email)
|
|
24
|
+
.focused($isFocused)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Multiple fields
|
|
28
|
+
|
|
29
|
+
```swift
|
|
30
|
+
enum Field: Hashable { case name, email, password }
|
|
31
|
+
@FocusState private var focusedField: Field?
|
|
32
|
+
|
|
33
|
+
TextField("Name", text: $name)
|
|
34
|
+
.focused($focusedField, equals: .name)
|
|
35
|
+
TextField("Email", text: $email)
|
|
36
|
+
.focused($focusedField, equals: .email)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Set `focusedField = .email` to move focus programmatically; set `nil` to dismiss the keyboard.
|
|
40
|
+
|
|
41
|
+
### `focused(_:)` vs `focused(_:equals:)` with nested views
|
|
42
|
+
|
|
43
|
+
`.focused($bool)` reports `true` when the modified view *or any focusable descendant* has focus. `.focused($enum, equals:)` reports its value only when that specific view receives focus.
|
|
44
|
+
|
|
45
|
+
```swift
|
|
46
|
+
enum Focus: Hashable { case container, field }
|
|
47
|
+
@FocusState private var focus: Focus?
|
|
48
|
+
|
|
49
|
+
VStack {
|
|
50
|
+
TextField("Name", text: $name)
|
|
51
|
+
.focused($focus, equals: .field)
|
|
52
|
+
}
|
|
53
|
+
.focusable()
|
|
54
|
+
.focused($focus, equals: .container)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
With `focused(_:equals:)` and a single `@FocusState`, SwiftUI distinguishes the container *receiving* focus from the container merely *containing* focus.
|
|
58
|
+
|
|
59
|
+
### `isFocused` environment value
|
|
60
|
+
|
|
61
|
+
Read-only environment value that returns `true` when the nearest focusable ancestor has focus. Useful for styling non-focusable child views.
|
|
62
|
+
|
|
63
|
+
```swift
|
|
64
|
+
struct HighlightWrapper: View {
|
|
65
|
+
@Environment(\.isFocused) private var isFocused
|
|
66
|
+
|
|
67
|
+
var body: some View {
|
|
68
|
+
content
|
|
69
|
+
.background(isFocused ? Color.accentColor.opacity(0.1) : .clear)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Making Views Focusable
|
|
75
|
+
|
|
76
|
+
### `.focusable(_:)`
|
|
77
|
+
|
|
78
|
+
Makes a non-text-input view participate in the focus system. Focused views can respond to keyboard events via `onKeyPress` and menu commands like Edit > Delete via `onDeleteCommand`.
|
|
79
|
+
|
|
80
|
+
```swift
|
|
81
|
+
struct SelectableCard: View {
|
|
82
|
+
@FocusState private var isFocused: Bool
|
|
83
|
+
|
|
84
|
+
var body: some View {
|
|
85
|
+
CardContent()
|
|
86
|
+
.focusable()
|
|
87
|
+
.focused($isFocused)
|
|
88
|
+
.border(isFocused ? Color.accentColor : .clear)
|
|
89
|
+
.onDeleteCommand { deleteCard() }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### `.focusable(_:interactions:)` (iOS 17+)
|
|
95
|
+
|
|
96
|
+
Controls which focus-driven interactions the view supports via `FocusInteractions`:
|
|
97
|
+
|
|
98
|
+
- `.activate` -- Button-like: only focusable when system-wide keyboard navigation is on (macOS/iOS)
|
|
99
|
+
- `.edit` -- Captures keyboard/Digital Crown input
|
|
100
|
+
- `.automatic` -- Platform default (both activate and edit)
|
|
101
|
+
|
|
102
|
+
```swift
|
|
103
|
+
MyTapGestureView(...)
|
|
104
|
+
.focusable(interactions: .activate)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Use `.activate` for custom button-like views that should match system keyboard-navigation behavior.
|
|
108
|
+
|
|
109
|
+
## Focused Values for Commands and Menus
|
|
110
|
+
|
|
111
|
+
Focused values let parent views (App, Scene, Commands) read state from whichever view currently has focus. Use for enabling/disabling menu commands based on the focused document or selection.
|
|
112
|
+
|
|
113
|
+
### Declare with `@Entry`
|
|
114
|
+
|
|
115
|
+
```swift
|
|
116
|
+
extension FocusedValues {
|
|
117
|
+
@Entry var selectedDocument: Binding<Document>?
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Focused values are typically optional (default is `nil` when no view publishes them), but you can also use non-optional entries when you have a sensible default value.
|
|
122
|
+
|
|
123
|
+
### Publish from views
|
|
124
|
+
|
|
125
|
+
```swift
|
|
126
|
+
// View-scoped: available when this view (or descendant) has focus
|
|
127
|
+
.focusedValue(\.selectedDocument, $document)
|
|
128
|
+
|
|
129
|
+
// Scene-scoped: available when this scene has focus
|
|
130
|
+
.focusedSceneValue(\.selectedDocument, $document)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Consume in commands
|
|
134
|
+
|
|
135
|
+
`@FocusedValue` reads the value; `@FocusedBinding` unwraps a `Binding` automatically.
|
|
136
|
+
|
|
137
|
+
```swift
|
|
138
|
+
@main
|
|
139
|
+
struct MyApp: App {
|
|
140
|
+
@FocusedBinding(\.selectedDocument) var document
|
|
141
|
+
|
|
142
|
+
var body: some Scene {
|
|
143
|
+
WindowGroup {
|
|
144
|
+
ContentView()
|
|
145
|
+
}
|
|
146
|
+
.commands {
|
|
147
|
+
CommandGroup(after: .pasteboard) {
|
|
148
|
+
Button("Duplicate") { document?.duplicate() }
|
|
149
|
+
.disabled(document == nil)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### `@FocusedObject` (iOS 16+)
|
|
157
|
+
|
|
158
|
+
For `ObservableObject` types. The view invalidates when the focused object changes.
|
|
159
|
+
|
|
160
|
+
```swift
|
|
161
|
+
// Publish
|
|
162
|
+
.focusedObject(myObservableModel)
|
|
163
|
+
|
|
164
|
+
// Consume
|
|
165
|
+
@FocusedObject var model: MyModel?
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Scene-scoped variant: `.focusedSceneObject(_:)`.
|
|
169
|
+
|
|
170
|
+
## Default Focus
|
|
171
|
+
|
|
172
|
+
### `.defaultFocus(_:_:priority:)` (iOS 17+, macOS 13+, tvOS 16+)
|
|
173
|
+
|
|
174
|
+
Prefer `.defaultFocus` over setting `@FocusState` in `onAppear` for initial focus placement.
|
|
175
|
+
|
|
176
|
+
```swift
|
|
177
|
+
@FocusState private var focusedField: Field?
|
|
178
|
+
|
|
179
|
+
VStack {
|
|
180
|
+
TextField("Name", text: $name)
|
|
181
|
+
.focused($focusedField, equals: .name)
|
|
182
|
+
TextField("Email", text: $email)
|
|
183
|
+
.focused($focusedField, equals: .email)
|
|
184
|
+
}
|
|
185
|
+
.defaultFocus($focusedField, .email)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Priority**: `.automatic` (default) applies on window appearance and programmatic focus changes. `.userInitiated` also applies during user-driven focus navigation.
|
|
189
|
+
|
|
190
|
+
### `prefersDefaultFocus(_:in:)` (macOS/tvOS/watchOS)
|
|
191
|
+
|
|
192
|
+
Used with `.focusScope(_:)` to mark a preferred default target within a scoped region.
|
|
193
|
+
|
|
194
|
+
### `resetFocus` environment action (macOS/tvOS/watchOS)
|
|
195
|
+
|
|
196
|
+
Re-evaluates default focus within a namespace.
|
|
197
|
+
|
|
198
|
+
```swift
|
|
199
|
+
@Namespace var scopeID
|
|
200
|
+
@Environment(\.resetFocus) private var resetFocus
|
|
201
|
+
|
|
202
|
+
Button("Reset") { resetFocus(in: scopeID) }
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Focus Scope and Sections
|
|
206
|
+
|
|
207
|
+
### `.focusScope(_:)` (macOS/tvOS/watchOS)
|
|
208
|
+
|
|
209
|
+
Limits default focus preferences to a namespace. Use with `prefersDefaultFocus` and `resetFocus`.
|
|
210
|
+
|
|
211
|
+
### `.focusSection()` (macOS 13+, tvOS 15+)
|
|
212
|
+
|
|
213
|
+
Guides directional and sequential focus movement through a group of focusable descendants. Useful when focusable views are spatially separated and directional navigation would otherwise skip them.
|
|
214
|
+
|
|
215
|
+
```swift
|
|
216
|
+
HStack {
|
|
217
|
+
VStack { Button("1") {}; Button("2") {}; Spacer() }
|
|
218
|
+
Spacer()
|
|
219
|
+
VStack { Spacer(); Button("A") {}; Button("B") {} }
|
|
220
|
+
.focusSection()
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Without `.focusSection()`, swiping right from buttons 1/2 finds nothing. With it, the VStack receives directional focus and delivers it to its first focusable child.
|
|
225
|
+
|
|
226
|
+
## Focus Effects
|
|
227
|
+
|
|
228
|
+
### `.focusEffectDisabled(_:)`
|
|
229
|
+
|
|
230
|
+
Suppresses the system focus ring (macOS) or hover effect. Use when providing custom focus visuals.
|
|
231
|
+
|
|
232
|
+
```swift
|
|
233
|
+
MyCustomCard()
|
|
234
|
+
.focusable()
|
|
235
|
+
.focusEffectDisabled()
|
|
236
|
+
.overlay { customFocusRing }
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`isFocusEffectEnabled` environment value reads the current state.
|
|
240
|
+
|
|
241
|
+
## Search Focus
|
|
242
|
+
|
|
243
|
+
### `.searchFocused(_:)` / `.searchFocused(_:equals:)`
|
|
244
|
+
|
|
245
|
+
Bind focus state to the search field associated with the nearest `.searchable` modifier. Works like `.focused` but targets the search bar.
|
|
246
|
+
|
|
247
|
+
```swift
|
|
248
|
+
@FocusState private var isSearchFocused: Bool
|
|
249
|
+
|
|
250
|
+
NavigationStack {
|
|
251
|
+
ContentView()
|
|
252
|
+
.searchable(text: $query)
|
|
253
|
+
.searchFocused($isSearchFocused)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Programmatically focus the search bar
|
|
257
|
+
Button("Search") { isSearchFocused = true }
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Common Pitfalls
|
|
261
|
+
|
|
262
|
+
### Redundant `@FocusState` writes revoke focus
|
|
263
|
+
|
|
264
|
+
`.focusable()` + `.focused()` handles focus-on-click natively. Adding a tap gesture that *also* writes to `@FocusState` triggers a redundant state write, causing a second body evaluation that revokes focus. The result: focus briefly appears then disappears, and key commands like `onDeleteCommand` stop working.
|
|
265
|
+
|
|
266
|
+
```swift
|
|
267
|
+
// WRONG -- tap gesture redundantly sets focus, causing double evaluation
|
|
268
|
+
CardView()
|
|
269
|
+
.focusable()
|
|
270
|
+
.focused($isFocused)
|
|
271
|
+
.onTapGesture { isFocused = true } // Remove this line
|
|
272
|
+
|
|
273
|
+
// CORRECT -- let .focusable() + .focused() handle it
|
|
274
|
+
CardView()
|
|
275
|
+
.focusable()
|
|
276
|
+
.focused($isFocused)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Ambiguous focus bindings
|
|
280
|
+
|
|
281
|
+
Binding the same enum case to multiple views is ambiguous. SwiftUI picks the first candidate and emits a runtime warning.
|
|
282
|
+
|
|
283
|
+
```swift
|
|
284
|
+
// WRONG -- .name bound to two views
|
|
285
|
+
TextField("Name", text: $name)
|
|
286
|
+
.focused($focusedField, equals: .name)
|
|
287
|
+
TextField("Full Name", text: $fullName)
|
|
288
|
+
.focused($focusedField, equals: .name) // ambiguous
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Always use distinct enum cases for each focusable view.
|
|
292
|
+
|
|
293
|
+
### `.onAppear` focus timing
|
|
294
|
+
|
|
295
|
+
Setting `@FocusState` in `.onAppear` may fail if the view tree hasn't settled. Prefer `.defaultFocus` (iOS 17+) for reliable initial focus. If you must use `.onAppear`, wrap in `DispatchQueue.main.async` as a last resort.
|
|
296
|
+
|
|
297
|
+
### Missing `.focusable()` for non-text views
|
|
298
|
+
|
|
299
|
+
`TextField` and `SecureField` are implicitly focusable. Custom views (stacks, shapes, images) are not. Forgetting `.focusable()` means `.focused()` bindings have no effect and key event handlers never fire.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# SwiftUI Image Optimization Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [AsyncImage Best Practices](#asyncimage-best-practices)
|
|
6
|
+
- [Image Decoding and Downsampling (Optional Optimization)](#image-decoding-and-downsampling-optional-optimization)
|
|
7
|
+
- [UIImage Loading and Memory](#uiimage-loading-and-memory)
|
|
8
|
+
- [SF Symbols](#sf-symbols)
|
|
9
|
+
- [Summary Checklist](#summary-checklist)
|
|
10
|
+
|
|
11
|
+
## AsyncImage Best Practices
|
|
12
|
+
|
|
13
|
+
### Basic AsyncImage with Phase Handling
|
|
14
|
+
|
|
15
|
+
```swift
|
|
16
|
+
// Good - handles loading and error states
|
|
17
|
+
AsyncImage(url: imageURL) { phase in
|
|
18
|
+
switch phase {
|
|
19
|
+
case .empty:
|
|
20
|
+
ProgressView()
|
|
21
|
+
case .success(let image):
|
|
22
|
+
image
|
|
23
|
+
.resizable()
|
|
24
|
+
.aspectRatio(contentMode: .fit)
|
|
25
|
+
case .failure:
|
|
26
|
+
Image(systemName: "photo")
|
|
27
|
+
.foregroundStyle(.secondary)
|
|
28
|
+
@unknown default:
|
|
29
|
+
EmptyView()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
.frame(width: 200, height: 200)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For custom placeholders, replace `ProgressView()` in the `.empty` case with your placeholder view. Add `.transition(.opacity)` to the success case and `.animation(.easeInOut, value: imageURL)` to the container for fade-in transitions.
|
|
36
|
+
|
|
37
|
+
## Image Decoding and Downsampling (Optional Optimization)
|
|
38
|
+
|
|
39
|
+
**When you encounter `UIImage(data:)` usage, consider suggesting image downsampling as a potential performance improvement**, especially for large images in lists or grids.
|
|
40
|
+
|
|
41
|
+
### Current Pattern That Could Be Optimized
|
|
42
|
+
|
|
43
|
+
```swift
|
|
44
|
+
// Current pattern - decodes full image on main thread
|
|
45
|
+
// Unsafe - force unwrap can crash if imageData is invalid
|
|
46
|
+
Image(uiImage: UIImage(data: imageData)!)
|
|
47
|
+
.resizable()
|
|
48
|
+
.aspectRatio(contentMode: .fit)
|
|
49
|
+
.frame(width: 200, height: 200)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Suggested Optimization Pattern
|
|
53
|
+
|
|
54
|
+
```swift
|
|
55
|
+
// Suggested optimization - decode and downsample off main thread
|
|
56
|
+
struct OptimizedImageView: View {
|
|
57
|
+
let imageData: Data
|
|
58
|
+
let targetSize: CGSize
|
|
59
|
+
@State private var processedImage: UIImage?
|
|
60
|
+
|
|
61
|
+
var body: some View {
|
|
62
|
+
Group {
|
|
63
|
+
if let processedImage {
|
|
64
|
+
Image(uiImage: processedImage)
|
|
65
|
+
.resizable()
|
|
66
|
+
.aspectRatio(contentMode: .fit)
|
|
67
|
+
} else {
|
|
68
|
+
ProgressView()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
.task {
|
|
72
|
+
processedImage = await decodeAndDownsample(imageData, targetSize: targetSize)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private func decodeAndDownsample(_ data: Data, targetSize: CGSize) async -> UIImage? {
|
|
77
|
+
await Task.detached {
|
|
78
|
+
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
|
|
79
|
+
return nil
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let options: [CFString: Any] = [
|
|
83
|
+
kCGImageSourceThumbnailMaxPixelSize: max(targetSize.width, targetSize.height),
|
|
84
|
+
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
|
85
|
+
kCGImageSourceCreateThumbnailWithTransform: true
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
|
|
89
|
+
return nil
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return UIImage(cgImage: cgImage)
|
|
93
|
+
}.value
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Usage
|
|
98
|
+
OptimizedImageView(
|
|
99
|
+
imageData: imageData,
|
|
100
|
+
targetSize: CGSize(width: 200, height: 200)
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Reusable Downsampling Actor
|
|
105
|
+
|
|
106
|
+
For production use, wrap the logic in an `actor` with scale-aware sizing and cache-disabled source options:
|
|
107
|
+
|
|
108
|
+
```swift
|
|
109
|
+
actor ImageProcessor {
|
|
110
|
+
func downsample(data: Data, targetSize: CGSize) -> UIImage? {
|
|
111
|
+
let scale = await UIScreen.main.scale
|
|
112
|
+
let maxPixel = max(targetSize.width, targetSize.height) * scale
|
|
113
|
+
let sourceOptions: [CFString: Any] = [kCGImageSourceShouldCache: false]
|
|
114
|
+
guard let source = CGImageSourceCreateWithData(data as CFData, sourceOptions as CFDictionary) else { return nil }
|
|
115
|
+
let downsampleOptions: [CFString: Any] = [
|
|
116
|
+
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
|
117
|
+
kCGImageSourceThumbnailMaxPixelSize: maxPixel,
|
|
118
|
+
kCGImageSourceCreateThumbnailWithTransform: true,
|
|
119
|
+
kCGImageSourceShouldCacheImmediately: true
|
|
120
|
+
]
|
|
121
|
+
guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions as CFDictionary) else { return nil }
|
|
122
|
+
return UIImage(cgImage: cgImage)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Key details: `kCGImageSourceShouldCache: false` on the source prevents the full-resolution image from being cached in memory. Multiplying `targetSize` by `UIScreen.main.scale` ensures the thumbnail is sharp on Retina displays. `kCGImageSourceShouldCacheImmediately: true` on the thumbnail forces decoding at creation time rather than at first render.
|
|
128
|
+
|
|
129
|
+
### When to Suggest This Optimization
|
|
130
|
+
|
|
131
|
+
Mention this optimization when you see `UIImage(data:)` usage, particularly in:
|
|
132
|
+
- Scrollable content (List, ScrollView with LazyVStack/LazyHStack)
|
|
133
|
+
- Grid layouts with many images
|
|
134
|
+
- Image galleries or carousels
|
|
135
|
+
- Any scenario where large images are displayed at smaller sizes
|
|
136
|
+
|
|
137
|
+
**Don't automatically apply it**—present it as an optional improvement for performance-sensitive scenarios.
|
|
138
|
+
|
|
139
|
+
## UIImage Loading and Memory
|
|
140
|
+
|
|
141
|
+
### UIImage(named:) Caches in System Cache
|
|
142
|
+
|
|
143
|
+
`UIImage(named:)` adds images to the system cache, which can cause memory spikes when loading many images (e.g., in a slider or gallery). For single-use or frequently-rotated images, use `UIImage(contentsOfFile:)` to bypass the cache:
|
|
144
|
+
|
|
145
|
+
```swift
|
|
146
|
+
// Caches in system cache -- memory builds up
|
|
147
|
+
let image = UIImage(named: "Wallpapers/image_001.jpg")
|
|
148
|
+
|
|
149
|
+
// No system caching -- memory stays flat
|
|
150
|
+
guard let path = Bundle.main.path(forResource: "Wallpapers/image_001.jpg", ofType: nil) else { return nil }
|
|
151
|
+
let image = UIImage(contentsOfFile: path)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### NSCache for Controlled Image Caching
|
|
155
|
+
|
|
156
|
+
When image processing (resizing, filtering) is needed, use `NSCache` with a `countLimit` to bound memory instead of relying on system caching:
|
|
157
|
+
|
|
158
|
+
```swift
|
|
159
|
+
struct ImageCache {
|
|
160
|
+
private let cache = NSCache<NSString, UIImage>()
|
|
161
|
+
|
|
162
|
+
init(countLimit: Int = 50) {
|
|
163
|
+
cache.countLimit = countLimit
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
subscript(key: String) -> UIImage? {
|
|
167
|
+
get { cache.object(forKey: key as NSString) }
|
|
168
|
+
nonmutating set {
|
|
169
|
+
if let newValue {
|
|
170
|
+
cache.setObject(newValue, forKey: key as NSString)
|
|
171
|
+
} else {
|
|
172
|
+
cache.removeObject(forKey: key as NSString)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## SF Symbols
|
|
180
|
+
|
|
181
|
+
```swift
|
|
182
|
+
Image(systemName: "star.fill")
|
|
183
|
+
.foregroundStyle(.yellow)
|
|
184
|
+
.symbolRenderingMode(.multicolor) // or .hierarchical, .palette, .monochrome
|
|
185
|
+
|
|
186
|
+
// Animated symbols (iOS 17+)
|
|
187
|
+
Image(systemName: "antenna.radiowaves.left.and.right")
|
|
188
|
+
.symbolEffect(.variableColor)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Variants are available via naming convention: `star.circle.fill`, `star.square.fill`, `folder.badge.plus`.
|
|
192
|
+
|
|
193
|
+
## Summary Checklist
|
|
194
|
+
|
|
195
|
+
- [ ] Use `AsyncImage` with proper phase handling
|
|
196
|
+
- [ ] Handle empty, success, and failure states
|
|
197
|
+
- [ ] Consider downsampling for `UIImage(data:)` in performance-sensitive scenarios
|
|
198
|
+
- [ ] Decode and downsample images off the main thread
|
|
199
|
+
- [ ] Use appropriate target sizes for downsampling
|
|
200
|
+
- [ ] Consider image caching for frequently accessed images
|
|
201
|
+
- [ ] Use SF Symbols with appropriate rendering modes
|
|
202
|
+
|
|
203
|
+
**Performance Note**: Image downsampling is an optional optimization. Only suggest it when you encounter `UIImage(data:)` usage in performance-sensitive contexts like scrollable lists or grids.
|