swift-code-reviewer-skill 1.2.0 → 1.2.1
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/CHANGELOG.md +35 -169
- package/README.md +43 -2
- package/SKILL.md +107 -742
- package/bin/install.js +1 -1
- package/package.json +2 -1
- package/references/companion-skills.md +70 -0
- package/skills/README.md +43 -0
- package/skills/swift-concurrency/NOTICE.md +18 -0
- package/skills/swift-concurrency/SKILL.md +235 -0
- package/skills/swift-concurrency/references/actors.md +640 -0
- package/skills/swift-concurrency/references/async-await-basics.md +249 -0
- package/skills/swift-concurrency/references/async-sequences.md +635 -0
- package/skills/swift-concurrency/references/core-data.md +533 -0
- package/skills/swift-concurrency/references/glossary.md +96 -0
- package/skills/swift-concurrency/references/linting.md +38 -0
- package/skills/swift-concurrency/references/memory-management.md +542 -0
- package/skills/swift-concurrency/references/migration.md +721 -0
- package/skills/swift-concurrency/references/performance.md +574 -0
- package/skills/swift-concurrency/references/sendable.md +578 -0
- package/skills/swift-concurrency/references/tasks.md +604 -0
- package/skills/swift-concurrency/references/testing.md +565 -0
- package/skills/swift-concurrency/references/threading.md +452 -0
- package/skills/swift-expert/NOTICE.md +18 -0
- package/skills/swift-expert/SKILL.md +226 -0
- package/skills/swift-expert/references/async-concurrency.md +363 -0
- package/skills/swift-expert/references/memory-performance.md +380 -0
- package/skills/swift-expert/references/protocol-oriented.md +357 -0
- package/skills/swift-expert/references/swiftui-patterns.md +294 -0
- package/skills/swift-expert/references/testing-patterns.md +402 -0
- package/skills/swift-testing/NOTICE.md +18 -0
- package/skills/swift-testing/SKILL.md +295 -0
- package/skills/swift-testing/references/async-testing.md +245 -0
- package/skills/swift-testing/references/dump-snapshot-testing.md +265 -0
- package/skills/swift-testing/references/fixtures.md +193 -0
- package/skills/swift-testing/references/integration-testing.md +189 -0
- package/skills/swift-testing/references/migration-xctest.md +301 -0
- package/skills/swift-testing/references/parameterized-tests.md +171 -0
- package/skills/swift-testing/references/snapshot-testing.md +201 -0
- package/skills/swift-testing/references/test-doubles.md +243 -0
- package/skills/swift-testing/references/test-organization.md +231 -0
- package/skills/swiftui-expert-skill/NOTICE.md +18 -0
- package/skills/swiftui-expert-skill/SKILL.md +281 -0
- package/skills/swiftui-expert-skill/references/accessibility-patterns.md +151 -0
- package/skills/swiftui-expert-skill/references/animation-advanced.md +403 -0
- package/skills/swiftui-expert-skill/references/animation-basics.md +284 -0
- package/skills/swiftui-expert-skill/references/animation-transitions.md +326 -0
- package/skills/swiftui-expert-skill/references/charts-accessibility.md +135 -0
- package/skills/swiftui-expert-skill/references/charts.md +602 -0
- package/skills/swiftui-expert-skill/references/image-optimization.md +203 -0
- package/skills/swiftui-expert-skill/references/latest-apis.md +464 -0
- package/skills/swiftui-expert-skill/references/layout-best-practices.md +266 -0
- package/skills/swiftui-expert-skill/references/liquid-glass.md +414 -0
- package/skills/swiftui-expert-skill/references/list-patterns.md +394 -0
- package/skills/swiftui-expert-skill/references/macos-scenes.md +318 -0
- package/skills/swiftui-expert-skill/references/macos-views.md +357 -0
- package/skills/swiftui-expert-skill/references/macos-window-styling.md +303 -0
- package/skills/swiftui-expert-skill/references/performance-patterns.md +403 -0
- package/skills/swiftui-expert-skill/references/scroll-patterns.md +293 -0
- package/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md +363 -0
- package/skills/swiftui-expert-skill/references/state-management.md +417 -0
- package/skills/swiftui-expert-skill/references/view-structure.md +389 -0
- package/skills/swiftui-ui-patterns/NOTICE.md +18 -0
- package/skills/swiftui-ui-patterns/SKILL.md +95 -0
- package/skills/swiftui-ui-patterns/references/app-wiring.md +201 -0
- package/skills/swiftui-ui-patterns/references/async-state.md +96 -0
- package/skills/swiftui-ui-patterns/references/components-index.md +50 -0
- package/skills/swiftui-ui-patterns/references/controls.md +57 -0
- package/skills/swiftui-ui-patterns/references/deeplinks.md +66 -0
- package/skills/swiftui-ui-patterns/references/focus.md +90 -0
- package/skills/swiftui-ui-patterns/references/form.md +97 -0
- package/skills/swiftui-ui-patterns/references/grids.md +71 -0
- package/skills/swiftui-ui-patterns/references/haptics.md +71 -0
- package/skills/swiftui-ui-patterns/references/input-toolbar.md +51 -0
- package/skills/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
- package/skills/swiftui-ui-patterns/references/list.md +86 -0
- package/skills/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
- package/skills/swiftui-ui-patterns/references/macos-settings.md +71 -0
- package/skills/swiftui-ui-patterns/references/matched-transitions.md +59 -0
- package/skills/swiftui-ui-patterns/references/media.md +73 -0
- package/skills/swiftui-ui-patterns/references/menu-bar.md +101 -0
- package/skills/swiftui-ui-patterns/references/navigationstack.md +159 -0
- package/skills/swiftui-ui-patterns/references/overlay.md +45 -0
- package/skills/swiftui-ui-patterns/references/performance.md +62 -0
- package/skills/swiftui-ui-patterns/references/previews.md +48 -0
- package/skills/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
- package/skills/swiftui-ui-patterns/references/scrollview.md +87 -0
- package/skills/swiftui-ui-patterns/references/searchable.md +71 -0
- package/skills/swiftui-ui-patterns/references/sheets.md +155 -0
- package/skills/swiftui-ui-patterns/references/split-views.md +72 -0
- package/skills/swiftui-ui-patterns/references/tabview.md +114 -0
- package/skills/swiftui-ui-patterns/references/theming.md +71 -0
- package/skills/swiftui-ui-patterns/references/title-menus.md +93 -0
- package/skills/swiftui-ui-patterns/references/top-bar.md +49 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
# SwiftUI State Management Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [Property Wrapper Selection Guide](#property-wrapper-selection-guide)
|
|
6
|
+
- [@State](#state)
|
|
7
|
+
- [Property Wrappers Inside @Observable Classes](#property-wrappers-inside-observable-classes)
|
|
8
|
+
- [@Binding](#binding)
|
|
9
|
+
- [@FocusState](#focusstate)
|
|
10
|
+
- [@StateObject vs @ObservedObject (Legacy - Pre-iOS 17)](#stateobject-vs-observedobject-legacy---pre-ios-17)
|
|
11
|
+
- [Don't Pass Values as @State](#dont-pass-values-as-state)
|
|
12
|
+
- [@Bindable (iOS 17+)](#bindable-ios-17)
|
|
13
|
+
- [let vs var for Passed Values](#let-vs-var-for-passed-values)
|
|
14
|
+
- [Environment and Preferences](#environment-and-preferences)
|
|
15
|
+
- [Decision Flowchart](#decision-flowchart)
|
|
16
|
+
- [State Privacy Rules](#state-privacy-rules)
|
|
17
|
+
- [Avoid Nested ObservableObject](#avoid-nested-observableobject)
|
|
18
|
+
- [Key Principles](#key-principles)
|
|
19
|
+
|
|
20
|
+
## Property Wrapper Selection Guide
|
|
21
|
+
|
|
22
|
+
| Wrapper | Use When | Notes |
|
|
23
|
+
|---------|----------|-------|
|
|
24
|
+
| `@State` | Internal view state that triggers updates | Must be `private` |
|
|
25
|
+
| `@Binding` | Child view needs to modify parent's state | Don't use for read-only |
|
|
26
|
+
| `@Bindable` | iOS 17+: View receives `@Observable` object and needs bindings | For injected observables |
|
|
27
|
+
| `let` | Read-only value passed from parent | Simplest option |
|
|
28
|
+
| `var` | Read-only value that child observes via `.onChange()` | For reactive reads |
|
|
29
|
+
|
|
30
|
+
**Legacy (Pre-iOS 17):**
|
|
31
|
+
| Wrapper | Use When | Notes |
|
|
32
|
+
|---------|----------|-------|
|
|
33
|
+
| `@StateObject` | View owns an `ObservableObject` instance | Use `@State` with `@Observable` instead |
|
|
34
|
+
| `@ObservedObject` | View receives an `ObservableObject` from outside | Never create inline |
|
|
35
|
+
|
|
36
|
+
## @State
|
|
37
|
+
|
|
38
|
+
Always mark `@State` properties as `private`. Use for internal view state that triggers UI updates.
|
|
39
|
+
|
|
40
|
+
```swift
|
|
41
|
+
// Correct
|
|
42
|
+
@State private var isAnimating = false
|
|
43
|
+
@State private var selectedTab = 0
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Why Private?** Marking state as `private` makes it clear what's created by the view versus what's passed in. It also prevents accidentally passing initial values that will be ignored (see "Don't Pass Values as @State" below).
|
|
47
|
+
|
|
48
|
+
### iOS 17+ with @Observable (Preferred)
|
|
49
|
+
|
|
50
|
+
**Always prefer `@Observable` over `ObservableObject`.** With iOS 17's `@Observable` macro, use `@State` instead of `@StateObject`:
|
|
51
|
+
|
|
52
|
+
```swift
|
|
53
|
+
@Observable
|
|
54
|
+
@MainActor // Always mark @Observable classes with @MainActor
|
|
55
|
+
final class DataModel {
|
|
56
|
+
var name = "Some Name"
|
|
57
|
+
var count = 0
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
struct MyView: View {
|
|
61
|
+
@State private var model = DataModel() // Use @State, not @StateObject
|
|
62
|
+
|
|
63
|
+
var body: some View {
|
|
64
|
+
VStack {
|
|
65
|
+
TextField("Name", text: $model.name)
|
|
66
|
+
Stepper("Count: \(model.count)", value: $model.count)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Critical**: When a view *owns* an `@Observable` object, always use `@State` -- not `let`. Without `@State`, SwiftUI may recreate the instance when a parent view redraws, losing accumulated state. `@State` tells SwiftUI to preserve the instance across view redraws. Using `@State` also provides bindings directly (no need for `@Bindable`).
|
|
73
|
+
|
|
74
|
+
**Note**: You may want to mark `@Observable` classes with `@MainActor` to ensure thread safety with SwiftUI, unless your project or package uses Default Actor Isolation set to `MainActor`—in which case, the explicit attribute is redundant and can be omitted.
|
|
75
|
+
|
|
76
|
+
## Property Wrappers Inside @Observable Classes
|
|
77
|
+
|
|
78
|
+
**Critical**: The `@Observable` macro transforms stored properties to add observation tracking. Property wrappers (like `@AppStorage`, `@SceneStorage`, `@Query`) also transform properties with their own storage. These two transformations conflict, causing a compiler error.
|
|
79
|
+
|
|
80
|
+
**Always annotate property-wrapper properties with `@ObservationIgnored` inside `@Observable` classes.**
|
|
81
|
+
|
|
82
|
+
```swift
|
|
83
|
+
@Observable
|
|
84
|
+
@MainActor
|
|
85
|
+
final class SettingsModel {
|
|
86
|
+
// WRONG - compiler error: property wrappers conflict with @Observable
|
|
87
|
+
// @AppStorage("username") var username = ""
|
|
88
|
+
|
|
89
|
+
// CORRECT - @ObservationIgnored prevents the conflict
|
|
90
|
+
@ObservationIgnored @AppStorage("username") var username = ""
|
|
91
|
+
@ObservationIgnored @AppStorage("isDarkMode") var isDarkMode = false
|
|
92
|
+
|
|
93
|
+
// Regular stored properties work fine with @Observable
|
|
94
|
+
var isLoading = false
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This applies to **any** property wrapper used inside an `@Observable` class, including but not limited to:
|
|
99
|
+
- `@AppStorage`
|
|
100
|
+
- `@SceneStorage`
|
|
101
|
+
- `@Query` (SwiftData)
|
|
102
|
+
|
|
103
|
+
**Note**: Since `@ObservationIgnored` disables observation tracking for that property, SwiftUI won't detect changes through the Observation framework. However, property wrappers like `@AppStorage` already notify SwiftUI of changes through their own mechanisms (e.g., UserDefaults KVO), so views still update correctly.
|
|
104
|
+
|
|
105
|
+
**Never remove `@ObservationIgnored`** from property-wrapper properties in `@Observable` classes — doing so causes a compiler error.
|
|
106
|
+
|
|
107
|
+
## @Binding
|
|
108
|
+
|
|
109
|
+
Use only when child view needs to **modify** parent's state. If child only reads the value, use `let` instead.
|
|
110
|
+
|
|
111
|
+
```swift
|
|
112
|
+
// Parent
|
|
113
|
+
struct ParentView: View {
|
|
114
|
+
@State private var isSelected = false
|
|
115
|
+
|
|
116
|
+
var body: some View {
|
|
117
|
+
ChildView(isSelected: $isSelected)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Child - will modify the value
|
|
122
|
+
struct ChildView: View {
|
|
123
|
+
@Binding var isSelected: Bool
|
|
124
|
+
|
|
125
|
+
var body: some View {
|
|
126
|
+
Button("Toggle") {
|
|
127
|
+
isSelected.toggle()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### When NOT to use @Binding
|
|
134
|
+
|
|
135
|
+
- **Don't use `@Binding` for read-only values.** If the child only displays the value and never modifies it, use `let` instead. `@Binding` adds unnecessary overhead and implies a write contract that doesn't exist.
|
|
136
|
+
|
|
137
|
+
## @FocusState
|
|
138
|
+
|
|
139
|
+
Use `@FocusState` to control text input focus in SwiftUI. Choose the focus value type based on how many fields the view manages.
|
|
140
|
+
|
|
141
|
+
### Bool vs enum
|
|
142
|
+
|
|
143
|
+
- Use `@FocusState private var isFocused: Bool` when the view has a single focusable field.
|
|
144
|
+
- Use a `Hashable` enum optional value for multiple fields, for better readability and type safety.
|
|
145
|
+
|
|
146
|
+
### Single Field: Bool
|
|
147
|
+
|
|
148
|
+
```swift
|
|
149
|
+
@FocusState private var isFocused: Bool
|
|
150
|
+
|
|
151
|
+
TextField("Email", text: $email)
|
|
152
|
+
.focused($isFocused)
|
|
153
|
+
.onAppear { isFocused = true }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Multiple Fields: Enum (Preferred)
|
|
157
|
+
|
|
158
|
+
Use a `Hashable` enum optional focus value when a view manages multiple fields.
|
|
159
|
+
|
|
160
|
+
```swift
|
|
161
|
+
enum Field: Hashable { case name, email, password }
|
|
162
|
+
@FocusState private var focusedField: Field?
|
|
163
|
+
|
|
164
|
+
TextField("Name", text: $name)
|
|
165
|
+
.focused($focusedField, equals: .name)
|
|
166
|
+
TextField("Email", text: $email)
|
|
167
|
+
.focused($focusedField, equals: .email)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Set `focusedField = .email` to move focus programmatically; set `nil` to dismiss the keyboard.
|
|
171
|
+
|
|
172
|
+
## @StateObject vs @ObservedObject (Legacy - Pre-iOS 17)
|
|
173
|
+
|
|
174
|
+
**Note**: Always prefer `@Observable` with `@State` for iOS 17+.
|
|
175
|
+
|
|
176
|
+
The key distinction is **ownership**: `@StateObject` when the view **creates and owns** the object; `@ObservedObject` when the view **receives** it from outside.
|
|
177
|
+
|
|
178
|
+
```swift
|
|
179
|
+
// View creates it → @StateObject
|
|
180
|
+
@StateObject private var viewModel = MyViewModel()
|
|
181
|
+
|
|
182
|
+
// View receives it → @ObservedObject
|
|
183
|
+
@ObservedObject var viewModel: MyViewModel
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Never** create an `ObservableObject` inline with `@ObservedObject` -- it recreates the instance on every view update.
|
|
187
|
+
|
|
188
|
+
### @StateObject instantiation in View's initializer
|
|
189
|
+
|
|
190
|
+
Prefer storing the `@StateObject` in the parent view and passing it down. If you must create one in a custom initializer, pass the expression directly to `StateObject(wrappedValue:)` so the `@autoclosure` prevents redundant allocations:
|
|
191
|
+
|
|
192
|
+
```swift
|
|
193
|
+
// Inside a View's init(movie:):
|
|
194
|
+
// WRONG — assigning to a local first defeats @autoclosure
|
|
195
|
+
let vm = MovieDetailsViewModel(movie: movie)
|
|
196
|
+
_viewModel = StateObject(wrappedValue: vm)
|
|
197
|
+
|
|
198
|
+
// CORRECT — inline expression defers creation
|
|
199
|
+
_viewModel = StateObject(wrappedValue: MovieDetailsViewModel(movie: movie))
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Modern Alternative**: Use `@Observable` with `@State` instead.
|
|
203
|
+
|
|
204
|
+
## Don't Pass Values as @State
|
|
205
|
+
|
|
206
|
+
**Critical**: Never declare passed values as `@State` or `@StateObject`. They only accept an initial value and ignore subsequent updates from the parent.
|
|
207
|
+
|
|
208
|
+
```swift
|
|
209
|
+
// WRONG - child ignores parent updates
|
|
210
|
+
struct ChildView: View {
|
|
211
|
+
@State var item: Item // Shows initial value forever!
|
|
212
|
+
var body: some View { Text(item.name) }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// CORRECT - child receives updates
|
|
216
|
+
struct ChildView: View {
|
|
217
|
+
let item: Item // Or @Binding if child needs to modify
|
|
218
|
+
var body: some View { Text(item.name) }
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Prevention**: Always mark `@State` and `@StateObject` as `private`. This prevents them from appearing in the generated initializer.
|
|
223
|
+
|
|
224
|
+
## @Bindable (iOS 17+)
|
|
225
|
+
|
|
226
|
+
Use when receiving an `@Observable` object from outside and needing bindings:
|
|
227
|
+
|
|
228
|
+
```swift
|
|
229
|
+
@Observable
|
|
230
|
+
final class UserModel {
|
|
231
|
+
var name = ""
|
|
232
|
+
var email = ""
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
struct ParentView: View {
|
|
236
|
+
@State private var user = UserModel()
|
|
237
|
+
|
|
238
|
+
var body: some View {
|
|
239
|
+
EditUserView(user: user)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
struct EditUserView: View {
|
|
244
|
+
@Bindable var user: UserModel // Received from parent, needs bindings
|
|
245
|
+
|
|
246
|
+
var body: some View {
|
|
247
|
+
Form {
|
|
248
|
+
TextField("Name", text: $user.name)
|
|
249
|
+
TextField("Email", text: $user.email)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## let vs var for Passed Values
|
|
256
|
+
|
|
257
|
+
### Use `let` for read-only display
|
|
258
|
+
|
|
259
|
+
```swift
|
|
260
|
+
struct ProfileHeader: View {
|
|
261
|
+
let username: String
|
|
262
|
+
let avatarURL: URL
|
|
263
|
+
|
|
264
|
+
var body: some View {
|
|
265
|
+
HStack {
|
|
266
|
+
AsyncImage(url: avatarURL)
|
|
267
|
+
Text(username)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Use `var` when reacting to changes with `.onChange()`
|
|
274
|
+
|
|
275
|
+
```swift
|
|
276
|
+
struct ReactiveView: View {
|
|
277
|
+
var externalValue: Int // Watch with .onChange()
|
|
278
|
+
@State private var displayText = ""
|
|
279
|
+
|
|
280
|
+
var body: some View {
|
|
281
|
+
Text(displayText)
|
|
282
|
+
.onChange(of: externalValue) { oldValue, newValue in
|
|
283
|
+
displayText = "Changed from \(oldValue) to \(newValue)"
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Environment and Preferences
|
|
290
|
+
|
|
291
|
+
### @Environment
|
|
292
|
+
|
|
293
|
+
Access environment values provided by SwiftUI or parent views:
|
|
294
|
+
|
|
295
|
+
```swift
|
|
296
|
+
struct MyView: View {
|
|
297
|
+
@Environment(\.colorScheme) private var colorScheme
|
|
298
|
+
@Environment(\.dismiss) private var dismiss
|
|
299
|
+
|
|
300
|
+
var body: some View {
|
|
301
|
+
Button("Done") { dismiss() }
|
|
302
|
+
.foregroundStyle(colorScheme == .dark ? .white : .black)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Custom Environment Values with @Entry
|
|
308
|
+
|
|
309
|
+
Use the `@Entry` macro (Xcode 16+, backward compatible to iOS 13) to define custom environment values without boilerplate:
|
|
310
|
+
|
|
311
|
+
```swift
|
|
312
|
+
extension EnvironmentValues {
|
|
313
|
+
@Entry var accentTheme: Theme = .default
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Inject
|
|
317
|
+
ContentView()
|
|
318
|
+
.environment(\.accentTheme, customTheme)
|
|
319
|
+
|
|
320
|
+
// Access
|
|
321
|
+
struct ThemedView: View {
|
|
322
|
+
@Environment(\.accentTheme) private var theme
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The `@Entry` macro replaces the manual `EnvironmentKey` conformance pattern. It also works with `TransactionValues`, `ContainerValues`, and `FocusedValues`.
|
|
327
|
+
|
|
328
|
+
### @Environment with @Observable (iOS 17+ - Preferred)
|
|
329
|
+
|
|
330
|
+
**Always prefer this pattern** for sharing state through the environment:
|
|
331
|
+
|
|
332
|
+
```swift
|
|
333
|
+
@Observable
|
|
334
|
+
@MainActor
|
|
335
|
+
final class AppState {
|
|
336
|
+
var isLoggedIn = false
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Inject
|
|
340
|
+
ContentView()
|
|
341
|
+
.environment(AppState())
|
|
342
|
+
|
|
343
|
+
// Access
|
|
344
|
+
struct ChildView: View {
|
|
345
|
+
@Environment(AppState.self) private var appState
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### @EnvironmentObject (Legacy - Pre-iOS 17)
|
|
350
|
+
|
|
351
|
+
Legacy pattern: inject with `.environmentObject(AppState())`, access with `@EnvironmentObject var appState: AppState`. Prefer `@Observable` with `@Environment` instead.
|
|
352
|
+
|
|
353
|
+
## Decision Flowchart
|
|
354
|
+
|
|
355
|
+
```
|
|
356
|
+
Is this value owned by this view?
|
|
357
|
+
├─ YES: Is it a simple value type?
|
|
358
|
+
│ ├─ YES → @State private var
|
|
359
|
+
│ └─ NO (class):
|
|
360
|
+
│ ├─ Use @Observable → @State private var (mark class @MainActor)
|
|
361
|
+
│ └─ Legacy ObservableObject → @StateObject private var
|
|
362
|
+
│
|
|
363
|
+
└─ NO (passed from parent):
|
|
364
|
+
├─ Does child need to MODIFY it?
|
|
365
|
+
│ ├─ YES → @Binding var
|
|
366
|
+
│ └─ NO: Does child need BINDINGS to its properties?
|
|
367
|
+
│ ├─ YES (@Observable) → @Bindable var
|
|
368
|
+
│ └─ NO: Does child react to changes?
|
|
369
|
+
│ ├─ YES → var + .onChange()
|
|
370
|
+
│ └─ NO → let
|
|
371
|
+
│
|
|
372
|
+
└─ Is it a legacy ObservableObject from parent?
|
|
373
|
+
└─ YES → @ObservedObject var (consider migrating to @Observable)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## State Privacy Rules
|
|
377
|
+
|
|
378
|
+
**All view-owned state should be `private`:**
|
|
379
|
+
|
|
380
|
+
```swift
|
|
381
|
+
// Correct - clear what's created vs passed
|
|
382
|
+
struct MyView: View {
|
|
383
|
+
// Created by view - private
|
|
384
|
+
@State private var isExpanded = false
|
|
385
|
+
@State private var viewModel = ViewModel()
|
|
386
|
+
@AppStorage("theme") private var theme = "light"
|
|
387
|
+
@Environment(\.colorScheme) private var colorScheme
|
|
388
|
+
|
|
389
|
+
// Passed from parent - not private
|
|
390
|
+
let title: String
|
|
391
|
+
@Binding var isSelected: Bool
|
|
392
|
+
@Bindable var user: User
|
|
393
|
+
|
|
394
|
+
var body: some View {
|
|
395
|
+
// ...
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**Why**: This makes dependencies explicit and improves code completion for the generated initializer.
|
|
401
|
+
|
|
402
|
+
## Avoid Nested ObservableObject
|
|
403
|
+
|
|
404
|
+
**Note**: This limitation only applies to `ObservableObject`. `@Observable` fully supports nested observed objects.
|
|
405
|
+
|
|
406
|
+
SwiftUI can't track changes through nested `ObservableObject` properties. Workaround: pass the nested object directly to child views as `@ObservedObject`. With `@Observable`, nesting works automatically.
|
|
407
|
+
|
|
408
|
+
## Key Principles
|
|
409
|
+
|
|
410
|
+
1. **Always prefer `@Observable` over `ObservableObject`** for new code
|
|
411
|
+
2. **Mark `@Observable` classes with `@MainActor` for thread safety (unless using default actor isolation)`**
|
|
412
|
+
3. Use `@State` with `@Observable` classes (not `@StateObject`)
|
|
413
|
+
4. Use `@Bindable` for injected `@Observable` objects that need bindings
|
|
414
|
+
5. **Always mark `@State` and `@StateObject` as `private`**
|
|
415
|
+
6. **Never declare passed values as `@State` or `@StateObject`**
|
|
416
|
+
7. With `@Observable`, nested objects work fine; with `ObservableObject`, pass nested objects directly to child views
|
|
417
|
+
8. **Always add `@ObservationIgnored` to property wrappers** (e.g., `@AppStorage`, `@SceneStorage`, `@Query`) inside `@Observable` classes — they conflict with the macro's property transformation
|