@kata-sh/cli 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/app-paths.d.ts +4 -0
- package/dist/app-paths.js +6 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +56 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +95 -0
- package/dist/resource-loader.d.ts +18 -0
- package/dist/resource-loader.js +50 -0
- package/dist/wizard.d.ts +15 -0
- package/dist/wizard.js +159 -0
- package/package.json +50 -21
- package/pkg/dist/modes/interactive/theme/dark.json +85 -0
- package/pkg/dist/modes/interactive/theme/light.json +84 -0
- package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/pkg/dist/modes/interactive/theme/theme.js +949 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
- package/pkg/package.json +8 -0
- package/scripts/postinstall.js +45 -0
- package/src/resources/AGENTS.md +108 -0
- package/src/resources/KATA-WORKFLOW.md +661 -0
- package/src/resources/agents/researcher.md +29 -0
- package/src/resources/agents/scout.md +56 -0
- package/src/resources/agents/worker.md +31 -0
- package/src/resources/extensions/ask-user-questions.ts +200 -0
- package/src/resources/extensions/bg-shell/index.ts +2758 -0
- package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
- package/src/resources/extensions/browser-tools/core.js +1057 -0
- package/src/resources/extensions/browser-tools/index.ts +4916 -0
- package/src/resources/extensions/browser-tools/package.json +20 -0
- package/src/resources/extensions/context7/index.ts +428 -0
- package/src/resources/extensions/context7/package.json +11 -0
- package/src/resources/extensions/get-secrets-from-user.ts +352 -0
- package/src/resources/extensions/github/formatters.ts +207 -0
- package/src/resources/extensions/github/gh-api.ts +537 -0
- package/src/resources/extensions/github/index.ts +778 -0
- package/src/resources/extensions/kata/activity-log.ts +88 -0
- package/src/resources/extensions/kata/auto.ts +2786 -0
- package/src/resources/extensions/kata/commands.ts +355 -0
- package/src/resources/extensions/kata/crash-recovery.ts +85 -0
- package/src/resources/extensions/kata/dashboard-overlay.ts +516 -0
- package/src/resources/extensions/kata/docs/preferences-reference.md +103 -0
- package/src/resources/extensions/kata/doctor.ts +683 -0
- package/src/resources/extensions/kata/files.ts +730 -0
- package/src/resources/extensions/kata/gitignore.ts +165 -0
- package/src/resources/extensions/kata/guided-flow.ts +976 -0
- package/src/resources/extensions/kata/index.ts +556 -0
- package/src/resources/extensions/kata/metrics.ts +397 -0
- package/src/resources/extensions/kata/observability-validator.ts +408 -0
- package/src/resources/extensions/kata/package.json +11 -0
- package/src/resources/extensions/kata/paths.ts +346 -0
- package/src/resources/extensions/kata/preferences.ts +695 -0
- package/src/resources/extensions/kata/prompt-loader.ts +50 -0
- package/src/resources/extensions/kata/prompts/complete-milestone.md +25 -0
- package/src/resources/extensions/kata/prompts/complete-slice.md +27 -0
- package/src/resources/extensions/kata/prompts/discuss.md +151 -0
- package/src/resources/extensions/kata/prompts/doctor-heal.md +29 -0
- package/src/resources/extensions/kata/prompts/execute-task.md +64 -0
- package/src/resources/extensions/kata/prompts/guided-complete-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-milestone.md +3 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-slice.md +59 -0
- package/src/resources/extensions/kata/prompts/guided-execute-task.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-plan-milestone.md +23 -0
- package/src/resources/extensions/kata/prompts/guided-plan-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-research-slice.md +11 -0
- package/src/resources/extensions/kata/prompts/guided-resume-task.md +1 -0
- package/src/resources/extensions/kata/prompts/plan-milestone.md +47 -0
- package/src/resources/extensions/kata/prompts/plan-slice.md +63 -0
- package/src/resources/extensions/kata/prompts/queue.md +85 -0
- package/src/resources/extensions/kata/prompts/reassess-roadmap.md +48 -0
- package/src/resources/extensions/kata/prompts/replan-slice.md +39 -0
- package/src/resources/extensions/kata/prompts/research-milestone.md +37 -0
- package/src/resources/extensions/kata/prompts/research-slice.md +28 -0
- package/src/resources/extensions/kata/prompts/run-uat.md +109 -0
- package/src/resources/extensions/kata/prompts/system.md +341 -0
- package/src/resources/extensions/kata/session-forensics.ts +550 -0
- package/src/resources/extensions/kata/skill-discovery.ts +137 -0
- package/src/resources/extensions/kata/state.ts +509 -0
- package/src/resources/extensions/kata/templates/context.md +76 -0
- package/src/resources/extensions/kata/templates/decisions.md +8 -0
- package/src/resources/extensions/kata/templates/milestone-summary.md +73 -0
- package/src/resources/extensions/kata/templates/plan.md +133 -0
- package/src/resources/extensions/kata/templates/preferences.md +15 -0
- package/src/resources/extensions/kata/templates/project.md +31 -0
- package/src/resources/extensions/kata/templates/reassessment.md +28 -0
- package/src/resources/extensions/kata/templates/requirements.md +81 -0
- package/src/resources/extensions/kata/templates/research.md +46 -0
- package/src/resources/extensions/kata/templates/roadmap.md +118 -0
- package/src/resources/extensions/kata/templates/slice-context.md +58 -0
- package/src/resources/extensions/kata/templates/slice-summary.md +99 -0
- package/src/resources/extensions/kata/templates/state.md +19 -0
- package/src/resources/extensions/kata/templates/task-plan.md +52 -0
- package/src/resources/extensions/kata/templates/task-summary.md +57 -0
- package/src/resources/extensions/kata/templates/uat.md +54 -0
- package/src/resources/extensions/kata/tests/activity-log-prune.test.ts +327 -0
- package/src/resources/extensions/kata/tests/auto-preflight.test.ts +97 -0
- package/src/resources/extensions/kata/tests/auto-supervisor.test.mjs +53 -0
- package/src/resources/extensions/kata/tests/complete-milestone.test.ts +317 -0
- package/src/resources/extensions/kata/tests/cost-projection.test.ts +160 -0
- package/src/resources/extensions/kata/tests/derive-state-deps.test.ts +477 -0
- package/src/resources/extensions/kata/tests/derive-state.test.ts +1013 -0
- package/src/resources/extensions/kata/tests/doctor.test.ts +718 -0
- package/src/resources/extensions/kata/tests/idle-recovery.test.ts +490 -0
- package/src/resources/extensions/kata/tests/metrics-io.test.ts +254 -0
- package/src/resources/extensions/kata/tests/metrics.test.ts +217 -0
- package/src/resources/extensions/kata/tests/must-have-parser.test.ts +309 -0
- package/src/resources/extensions/kata/tests/parsers.test.ts +1257 -0
- package/src/resources/extensions/kata/tests/plan-milestone.test.ts +185 -0
- package/src/resources/extensions/kata/tests/plan-quality-validator.test.ts +386 -0
- package/src/resources/extensions/kata/tests/reassess-prompt.test.ts +208 -0
- package/src/resources/extensions/kata/tests/replan-slice.test.ts +686 -0
- package/src/resources/extensions/kata/tests/requirements.test.ts +151 -0
- package/src/resources/extensions/kata/tests/resolve-ts-hooks.mjs +17 -0
- package/src/resources/extensions/kata/tests/resolve-ts.mjs +11 -0
- package/src/resources/extensions/kata/tests/run-uat.test.ts +383 -0
- package/src/resources/extensions/kata/tests/unit-runtime.test.ts +388 -0
- package/src/resources/extensions/kata/tests/workspace-index.test.ts +118 -0
- package/src/resources/extensions/kata/tests/worktree.test.ts +222 -0
- package/src/resources/extensions/kata/types.ts +159 -0
- package/src/resources/extensions/kata/unit-runtime.ts +163 -0
- package/src/resources/extensions/kata/workspace-index.ts +203 -0
- package/src/resources/extensions/kata/worktree.ts +182 -0
- package/src/resources/extensions/mac-tools/index.ts +852 -0
- package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
- package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
- package/src/resources/extensions/search-the-web/cache.ts +78 -0
- package/src/resources/extensions/search-the-web/format.ts +258 -0
- package/src/resources/extensions/search-the-web/http.ts +238 -0
- package/src/resources/extensions/search-the-web/index.ts +68 -0
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +404 -0
- package/src/resources/extensions/search-the-web/tool-search.ts +503 -0
- package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
- package/src/resources/extensions/shared/confirm-ui.ts +126 -0
- package/src/resources/extensions/shared/interview-ui.ts +822 -0
- package/src/resources/extensions/shared/next-action-ui.ts +235 -0
- package/src/resources/extensions/shared/progress-widget.ts +282 -0
- package/src/resources/extensions/shared/thinking-widget.ts +107 -0
- package/src/resources/extensions/shared/ui.ts +400 -0
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/slash-commands/audit.ts +92 -0
- package/src/resources/extensions/slash-commands/create-extension.ts +375 -0
- package/src/resources/extensions/slash-commands/create-slash-command.ts +280 -0
- package/src/resources/extensions/slash-commands/index.ts +12 -0
- package/src/resources/extensions/slash-commands/kata-run.ts +34 -0
- package/src/resources/extensions/subagent/agents.ts +126 -0
- package/src/resources/extensions/subagent/index.ts +1293 -0
- package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
- package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
- package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
- package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
- package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
- package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
- package/src/resources/skills/frontend-design/SKILL.md +45 -0
- package/src/resources/skills/swiftui/SKILL.md +208 -0
- package/src/resources/skills/swiftui/references/animations.md +921 -0
- package/src/resources/skills/swiftui/references/architecture.md +1561 -0
- package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
- package/src/resources/skills/swiftui/references/navigation.md +1492 -0
- package/src/resources/skills/swiftui/references/networking-async.md +214 -0
- package/src/resources/skills/swiftui/references/performance.md +1706 -0
- package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
- package/src/resources/skills/swiftui/references/state-management.md +1443 -0
- package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
- package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
- package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
- package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
- package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
- package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
- package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
- package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
- package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
- package/dist/commands/task.d.ts +0 -9
- package/dist/commands/task.d.ts.map +0 -1
- package/dist/commands/task.js +0 -129
- package/dist/commands/task.js.map +0 -1
- package/dist/commands/task.test.d.ts +0 -2
- package/dist/commands/task.test.d.ts.map +0 -1
- package/dist/commands/task.test.js +0 -169
- package/dist/commands/task.test.js.map +0 -1
- package/dist/e2e/task-e2e.test.d.ts +0 -2
- package/dist/e2e/task-e2e.test.d.ts.map +0 -1
- package/dist/e2e/task-e2e.test.js +0 -173
- package/dist/e2e/task-e2e.test.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -93
- package/dist/index.js.map +0 -1
- package/dist/slug.d.ts +0 -2
- package/dist/slug.d.ts.map +0 -1
- package/dist/slug.js +0 -12
- package/dist/slug.js.map +0 -1
- package/dist/slug.test.d.ts +0 -2
- package/dist/slug.test.d.ts.map +0 -1
- package/dist/slug.test.js +0 -32
- package/dist/slug.test.js.map +0 -1
|
@@ -0,0 +1,1443 @@
|
|
|
1
|
+
<overview>
|
|
2
|
+
SwiftUI state management is fundamentally different from imperative UI frameworks. You describe what the UI should look like for any given state, and SwiftUI handles the updates when state changes.
|
|
3
|
+
|
|
4
|
+
**Read this file when:** Building views that need to respond to data changes, sharing data between views, choosing property wrappers, debugging state issues, or migrating from ObservableObject patterns.
|
|
5
|
+
|
|
6
|
+
**Key insight:** SwiftUI uses a declarative, unidirectional data flow. State flows down through the view hierarchy via properties. Changes flow up through bindings or actions. You describe state, not mutations.
|
|
7
|
+
|
|
8
|
+
**Modern SwiftUI (iOS 17+)** uses the @Observable macro for reference types, eliminating most needs for ObservableObject, @Published, @StateObject, @ObservedObject, and @EnvironmentObject. The mental model is simpler: value types use @State/@Binding, reference types use @Observable with @State/@Bindable.
|
|
9
|
+
</overview>
|
|
10
|
+
|
|
11
|
+
<property_wrappers>
|
|
12
|
+
## Property Wrappers
|
|
13
|
+
|
|
14
|
+
<wrapper name="@State">
|
|
15
|
+
**Purpose:** Manage mutable value types owned by a single view. The source of truth for simple, view-local data.
|
|
16
|
+
|
|
17
|
+
**When to use:** Simple values (Int, String, Bool, Array, struct) that belong to this view and need to trigger UI updates when changed.
|
|
18
|
+
|
|
19
|
+
**Ownership:** The view owns this data. SwiftUI manages its lifecycle.
|
|
20
|
+
|
|
21
|
+
**Lifecycle:** Persists across view body recomputes. Reset when the view is removed from the hierarchy and recreated with a new identity.
|
|
22
|
+
|
|
23
|
+
```swift
|
|
24
|
+
struct CounterView: View {
|
|
25
|
+
@State private var count = 0
|
|
26
|
+
@State private var isExpanded = false
|
|
27
|
+
|
|
28
|
+
var body: some View {
|
|
29
|
+
VStack {
|
|
30
|
+
Text("Count: \(count)")
|
|
31
|
+
|
|
32
|
+
Button("Increment") {
|
|
33
|
+
count += 1
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if isExpanded {
|
|
37
|
+
Text("Details about count...")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Toggle("Show Details", isOn: $isExpanded)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**With @Observable classes (iOS 17+):**
|
|
47
|
+
```swift
|
|
48
|
+
@Observable
|
|
49
|
+
class ViewModel {
|
|
50
|
+
var items: [String] = []
|
|
51
|
+
var selectedItem: String?
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
struct ContentView: View {
|
|
55
|
+
@State private var viewModel = ViewModel()
|
|
56
|
+
|
|
57
|
+
var body: some View {
|
|
58
|
+
List(viewModel.items, id: \.self) { item in
|
|
59
|
+
Text(item)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Common mistakes:**
|
|
66
|
+
- Making @State public (should be private to enforce view-local ownership)
|
|
67
|
+
- Using @State for data passed from a parent (use @Binding or receive as plain property)
|
|
68
|
+
- Not initializing @State with a value
|
|
69
|
+
- Using @State with ObservableObject classes pre-iOS 17 (use @StateObject instead)
|
|
70
|
+
</wrapper>
|
|
71
|
+
|
|
72
|
+
<wrapper name="@Binding">
|
|
73
|
+
**Purpose:** Create a two-way connection to state owned by another view. Allows a child view to read and write a parent's state without owning it.
|
|
74
|
+
|
|
75
|
+
**When to use:** Passing writable access to value type data from parent to child. The child needs to modify data it doesn't own.
|
|
76
|
+
|
|
77
|
+
**Ownership:** The parent owns the data. This view has read-write access via reference.
|
|
78
|
+
|
|
79
|
+
**Lifecycle:** Tied to the source of truth it references.
|
|
80
|
+
|
|
81
|
+
```swift
|
|
82
|
+
struct ParentView: View {
|
|
83
|
+
@State private var username = ""
|
|
84
|
+
|
|
85
|
+
var body: some View {
|
|
86
|
+
VStack {
|
|
87
|
+
Text("Hello, \(username)")
|
|
88
|
+
UsernameField(username: $username)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
struct UsernameField: View {
|
|
94
|
+
@Binding var username: String
|
|
95
|
+
|
|
96
|
+
var body: some View {
|
|
97
|
+
TextField("Enter name", text: $username)
|
|
98
|
+
.textFieldStyle(.roundedBorder)
|
|
99
|
+
.padding()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**With custom controls:**
|
|
105
|
+
```swift
|
|
106
|
+
struct ToggleButton: View {
|
|
107
|
+
@Binding var isOn: Bool
|
|
108
|
+
let label: String
|
|
109
|
+
|
|
110
|
+
var body: some View {
|
|
111
|
+
Button(label) {
|
|
112
|
+
isOn.toggle()
|
|
113
|
+
}
|
|
114
|
+
.foregroundStyle(isOn ? .green : .gray)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Usage
|
|
119
|
+
struct ContentView: View {
|
|
120
|
+
@State private var notificationsEnabled = false
|
|
121
|
+
|
|
122
|
+
var body: some View {
|
|
123
|
+
ToggleButton(
|
|
124
|
+
isOn: $notificationsEnabled,
|
|
125
|
+
label: "Notifications"
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Common mistakes:**
|
|
132
|
+
- Providing a default value to @Binding (bindings are always passed from outside)
|
|
133
|
+
- Making @Binding private (it must be accessible to receive the binding)
|
|
134
|
+
- Passing the value without $ prefix (passes a copy, not a binding)
|
|
135
|
+
- Using @Binding when the child shouldn't modify the value (use a plain property instead)
|
|
136
|
+
</wrapper>
|
|
137
|
+
|
|
138
|
+
<wrapper name="@Observable">
|
|
139
|
+
**Purpose:** Mark a class as observable so SwiftUI automatically tracks property changes and updates views. Replaces ObservableObject protocol in iOS 17+.
|
|
140
|
+
|
|
141
|
+
**When to use:** Reference type data models shared across multiple views. Complex state that benefits from reference semantics. Data that needs to be passed down the view hierarchy.
|
|
142
|
+
|
|
143
|
+
**Ownership:** Created and owned by a view using @State, or passed through @Environment for app-wide access.
|
|
144
|
+
|
|
145
|
+
**Lifecycle:** Follows standard Swift reference type lifecycle. When stored in @State, survives view body recomputes.
|
|
146
|
+
|
|
147
|
+
```swift
|
|
148
|
+
import Observation
|
|
149
|
+
|
|
150
|
+
@Observable
|
|
151
|
+
class ShoppingCart {
|
|
152
|
+
var items: [Item] = []
|
|
153
|
+
var discount: Double = 0.0
|
|
154
|
+
|
|
155
|
+
var total: Double {
|
|
156
|
+
let subtotal = items.reduce(0) { $0 + $1.price }
|
|
157
|
+
return subtotal * (1 - discount)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
func addItem(_ item: Item) {
|
|
161
|
+
items.append(item)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
struct StoreView: View {
|
|
166
|
+
@State private var cart = ShoppingCart()
|
|
167
|
+
|
|
168
|
+
var body: some View {
|
|
169
|
+
VStack {
|
|
170
|
+
CartSummary(cart: cart)
|
|
171
|
+
ProductList(cart: cart)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
struct CartSummary: View {
|
|
177
|
+
var cart: ShoppingCart // Plain property, no wrapper needed
|
|
178
|
+
|
|
179
|
+
var body: some View {
|
|
180
|
+
Text("Total: $\(cart.total, specifier: "%.2f")")
|
|
181
|
+
.font(.headline)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**With @ObservationIgnored for non-tracked properties:**
|
|
187
|
+
```swift
|
|
188
|
+
@Observable
|
|
189
|
+
class UserSession {
|
|
190
|
+
var username: String = ""
|
|
191
|
+
var loginCount: Int = 0
|
|
192
|
+
|
|
193
|
+
@ObservationIgnored
|
|
194
|
+
var temporaryCache: [String: Any] = [:] // Won't trigger view updates
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Common mistakes:**
|
|
199
|
+
- Using @Published with @Observable (not needed, all properties are observed by default)
|
|
200
|
+
- Forgetting to import Observation
|
|
201
|
+
- Using @StateObject instead of @State for @Observable classes
|
|
202
|
+
- Not using @ObservationIgnored for properties that shouldn't trigger updates (like caches, formatters)
|
|
203
|
+
</wrapper>
|
|
204
|
+
|
|
205
|
+
<wrapper name="@Bindable">
|
|
206
|
+
**Purpose:** Create bindings to properties of @Observable objects when the view doesn't own the object. Bridges @Observable with SwiftUI's $ binding syntax.
|
|
207
|
+
|
|
208
|
+
**When to use:** You have an @Observable object passed from a parent, and you need to create two-way bindings to its properties (for TextField, Toggle, etc.).
|
|
209
|
+
|
|
210
|
+
**Ownership:** The view doesn't own the object. It's passed from outside.
|
|
211
|
+
|
|
212
|
+
**Lifecycle:** Tied to the lifecycle of the @Observable object it references.
|
|
213
|
+
|
|
214
|
+
```swift
|
|
215
|
+
@Observable
|
|
216
|
+
class FormData {
|
|
217
|
+
var name: String = ""
|
|
218
|
+
var email: String = ""
|
|
219
|
+
var agreedToTerms: Bool = false
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
struct ParentView: View {
|
|
223
|
+
@State private var formData = FormData()
|
|
224
|
+
|
|
225
|
+
var body: some View {
|
|
226
|
+
FormView(formData: formData)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
struct FormView: View {
|
|
231
|
+
@Bindable var formData: FormData
|
|
232
|
+
|
|
233
|
+
var body: some View {
|
|
234
|
+
Form {
|
|
235
|
+
TextField("Name", text: $formData.name)
|
|
236
|
+
TextField("Email", text: $formData.email)
|
|
237
|
+
Toggle("I agree to terms", isOn: $formData.agreedToTerms)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Nested child views:**
|
|
244
|
+
```swift
|
|
245
|
+
struct NestedChildView: View {
|
|
246
|
+
@Bindable var formData: FormData
|
|
247
|
+
|
|
248
|
+
var body: some View {
|
|
249
|
+
// Can still create bindings to properties
|
|
250
|
+
Toggle("Marketing emails", isOn: $formData.agreedToTerms)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Common mistakes:**
|
|
256
|
+
- Using @Bindable when you own the object (use @State instead)
|
|
257
|
+
- Using @Binding for @Observable objects (use @Bindable for reference types)
|
|
258
|
+
- Forgetting that @Bindable doesn't work with ObservableObject (legacy pattern)
|
|
259
|
+
- Using @ObservedObject instead of @Bindable for iOS 17+ code
|
|
260
|
+
</wrapper>
|
|
261
|
+
|
|
262
|
+
<wrapper name="@Environment">
|
|
263
|
+
**Purpose:** Read values from SwiftUI's environment or inject custom values accessible throughout the view hierarchy. Replaces @EnvironmentObject in iOS 17+.
|
|
264
|
+
|
|
265
|
+
**When to use:**
|
|
266
|
+
- Accessing system values (colorScheme, locale, dismiss, etc.)
|
|
267
|
+
- Sharing app-wide or subtree-wide state without prop drilling
|
|
268
|
+
- Dependency injection for services and models
|
|
269
|
+
|
|
270
|
+
**Ownership:** Provided by ancestor views or the system. Current view reads it.
|
|
271
|
+
|
|
272
|
+
**Lifecycle:** Managed by the provider. Available to all descendant views.
|
|
273
|
+
|
|
274
|
+
```swift
|
|
275
|
+
// System environment values
|
|
276
|
+
struct ThemedView: View {
|
|
277
|
+
@Environment(\.colorScheme) var colorScheme
|
|
278
|
+
@Environment(\.dismiss) var dismiss
|
|
279
|
+
|
|
280
|
+
var body: some View {
|
|
281
|
+
VStack {
|
|
282
|
+
Text("Current theme: \(colorScheme == .dark ? "Dark" : "Light")")
|
|
283
|
+
|
|
284
|
+
Button("Close") {
|
|
285
|
+
dismiss()
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
.foregroundStyle(colorScheme == .dark ? .white : .black)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**Custom environment values (iOS 17+):**
|
|
294
|
+
```swift
|
|
295
|
+
@Observable
|
|
296
|
+
class AppSettings {
|
|
297
|
+
var fontSize: Double = 16
|
|
298
|
+
var accentColor: Color = .blue
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// In your app root
|
|
302
|
+
@main
|
|
303
|
+
struct MyApp: App {
|
|
304
|
+
@State private var settings = AppSettings()
|
|
305
|
+
|
|
306
|
+
var body: some Scene {
|
|
307
|
+
WindowGroup {
|
|
308
|
+
ContentView()
|
|
309
|
+
.environment(settings)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Access in any descendant view
|
|
315
|
+
struct SettingsView: View {
|
|
316
|
+
@Environment(AppSettings.self) var settings
|
|
317
|
+
|
|
318
|
+
var body: some View {
|
|
319
|
+
VStack {
|
|
320
|
+
Text("Font size: \(settings.fontSize)")
|
|
321
|
+
ColorPicker("Accent", selection: $settings.accentColor)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Legacy custom environment values (pre-iOS 17):**
|
|
328
|
+
```swift
|
|
329
|
+
private struct ThemeKey: EnvironmentKey {
|
|
330
|
+
static let defaultValue = Theme.light
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
extension EnvironmentValues {
|
|
334
|
+
var theme: Theme {
|
|
335
|
+
get { self[ThemeKey.self] }
|
|
336
|
+
set { self[ThemeKey.self] = newValue }
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Usage
|
|
341
|
+
struct ContentView: View {
|
|
342
|
+
@Environment(\.theme) var theme
|
|
343
|
+
|
|
344
|
+
var body: some View {
|
|
345
|
+
Text("Hello")
|
|
346
|
+
.foregroundStyle(theme.textColor)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**Common mistakes:**
|
|
352
|
+
- Using @EnvironmentObject instead of @Environment for iOS 17+ code
|
|
353
|
+
- Not providing the environment value before accessing it (runtime crash)
|
|
354
|
+
- Overusing environment for data that should be passed as properties
|
|
355
|
+
- Using environment for frequently changing values (can cause unnecessary updates)
|
|
356
|
+
</wrapper>
|
|
357
|
+
|
|
358
|
+
<wrapper name="@AppStorage">
|
|
359
|
+
**Purpose:** Read and write UserDefaults values with automatic UI updates when the value changes.
|
|
360
|
+
|
|
361
|
+
**When to use:** Storing user preferences, settings, or small amounts of persistent data that should survive app relaunches.
|
|
362
|
+
|
|
363
|
+
**Ownership:** Backed by UserDefaults. View has read-write access.
|
|
364
|
+
|
|
365
|
+
**Lifecycle:** Persists between app launches until explicitly removed.
|
|
366
|
+
|
|
367
|
+
```swift
|
|
368
|
+
struct SettingsView: View {
|
|
369
|
+
@AppStorage("username") private var username = "Guest"
|
|
370
|
+
@AppStorage("notificationsEnabled") private var notificationsEnabled = true
|
|
371
|
+
@AppStorage("theme") private var theme = "system"
|
|
372
|
+
|
|
373
|
+
var body: some View {
|
|
374
|
+
Form {
|
|
375
|
+
TextField("Username", text: $username)
|
|
376
|
+
|
|
377
|
+
Toggle("Notifications", isOn: $notificationsEnabled)
|
|
378
|
+
|
|
379
|
+
Picker("Theme", selection: $theme) {
|
|
380
|
+
Text("System").tag("system")
|
|
381
|
+
Text("Light").tag("light")
|
|
382
|
+
Text("Dark").tag("dark")
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
**With custom UserDefaults suite:**
|
|
390
|
+
```swift
|
|
391
|
+
struct SharedSettingsView: View {
|
|
392
|
+
@AppStorage("syncEnabled", store: UserDefaults(suiteName: "group.com.example.app"))
|
|
393
|
+
private var syncEnabled = false
|
|
394
|
+
|
|
395
|
+
var body: some View {
|
|
396
|
+
Toggle("Sync", isOn: $syncEnabled)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Supported types:** Bool, Int, Double, String, URL, Data
|
|
402
|
+
|
|
403
|
+
**Common mistakes:**
|
|
404
|
+
- Storing sensitive data (UserDefaults is not encrypted)
|
|
405
|
+
- Storing large amounts of data (performance degradation)
|
|
406
|
+
- Using for data that changes frequently during a session (use @State instead)
|
|
407
|
+
- Not providing a default value
|
|
408
|
+
- Assuming cross-app synchronization (requires App Groups configuration)
|
|
409
|
+
</wrapper>
|
|
410
|
+
|
|
411
|
+
<wrapper name="@SceneStorage">
|
|
412
|
+
**Purpose:** Automatic state restoration per scene. Saves and restores values when the app is backgrounded/foregrounded or scenes are destroyed/recreated.
|
|
413
|
+
|
|
414
|
+
**When to use:** Preserving UI state for state restoration (selected tab, scroll position, current navigation path, form data).
|
|
415
|
+
|
|
416
|
+
**Ownership:** Managed per scene by the system. View has read-write access.
|
|
417
|
+
|
|
418
|
+
**Lifecycle:** Persists when app backgrounds. Destroyed when user explicitly kills the app from the app switcher.
|
|
419
|
+
|
|
420
|
+
```swift
|
|
421
|
+
struct ContentView: View {
|
|
422
|
+
@SceneStorage("selectedTab") private var selectedTab = "home"
|
|
423
|
+
|
|
424
|
+
var body: some View {
|
|
425
|
+
TabView(selection: $selectedTab) {
|
|
426
|
+
HomeView()
|
|
427
|
+
.tabItem { Label("Home", systemImage: "house") }
|
|
428
|
+
.tag("home")
|
|
429
|
+
|
|
430
|
+
ProfileView()
|
|
431
|
+
.tabItem { Label("Profile", systemImage: "person") }
|
|
432
|
+
.tag("profile")
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
**With navigation state:**
|
|
439
|
+
```swift
|
|
440
|
+
struct NavigationExample: View {
|
|
441
|
+
@SceneStorage("navigationPath") private var navigationPathData: Data?
|
|
442
|
+
@State private var path = NavigationPath()
|
|
443
|
+
|
|
444
|
+
var body: some View {
|
|
445
|
+
NavigationStack(path: $path) {
|
|
446
|
+
List {
|
|
447
|
+
NavigationLink("Details", value: "details")
|
|
448
|
+
}
|
|
449
|
+
.navigationDestination(for: String.self) { value in
|
|
450
|
+
Text("Showing: \(value)")
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
.onAppear {
|
|
454
|
+
if let data = navigationPathData,
|
|
455
|
+
let restored = try? JSONDecoder().decode(NavigationPath.CodableRepresentation.self, from: data) {
|
|
456
|
+
path = NavigationPath(restored)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
.onChange(of: path) { _, newPath in
|
|
460
|
+
if let representation = newPath.codable,
|
|
461
|
+
let data = try? JSONEncoder().encode(representation) {
|
|
462
|
+
navigationPathData = data
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
**Supported types:** Bool, Int, Double, String, URL, Data
|
|
470
|
+
|
|
471
|
+
**Common mistakes:**
|
|
472
|
+
- Storing sensitive data (not secure)
|
|
473
|
+
- Storing large amounts of data (Apple warns against this)
|
|
474
|
+
- Expecting data to persist after force-quit (it's cleared)
|
|
475
|
+
- Using for cross-scene data (each scene has its own storage)
|
|
476
|
+
- Not providing a default value
|
|
477
|
+
</wrapper>
|
|
478
|
+
|
|
479
|
+
<wrapper name="@StateObject">
|
|
480
|
+
**Purpose:** Create and own an ObservableObject in a view. Ensures the object survives view body recomputes.
|
|
481
|
+
|
|
482
|
+
**When to use:** Legacy code (pre-iOS 17) when you need to create and own an ObservableObject. For iOS 17+, use @State with @Observable instead.
|
|
483
|
+
|
|
484
|
+
**Ownership:** The view owns and manages the object's lifecycle.
|
|
485
|
+
|
|
486
|
+
**Lifecycle:** Created once when the view is initialized. Survives view body recomputes. Destroyed when view is removed.
|
|
487
|
+
|
|
488
|
+
```swift
|
|
489
|
+
// Legacy pattern (pre-iOS 17)
|
|
490
|
+
class LegacyViewModel: ObservableObject {
|
|
491
|
+
@Published var count = 0
|
|
492
|
+
@Published var items: [String] = []
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
struct LegacyView: View {
|
|
496
|
+
@StateObject private var viewModel = LegacyViewModel()
|
|
497
|
+
|
|
498
|
+
var body: some View {
|
|
499
|
+
VStack {
|
|
500
|
+
Text("Count: \(viewModel.count)")
|
|
501
|
+
|
|
502
|
+
Button("Increment") {
|
|
503
|
+
viewModel.count += 1
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
**Common mistakes:**
|
|
511
|
+
- Using @StateObject for iOS 17+ (use @State with @Observable instead)
|
|
512
|
+
- Using @ObservedObject when the view creates the object (causes recreation bugs)
|
|
513
|
+
- Creating @StateObject in non-root views unnecessarily (consider passing from parent)
|
|
514
|
+
- Using for value types (use @State instead)
|
|
515
|
+
</wrapper>
|
|
516
|
+
|
|
517
|
+
<wrapper name="@ObservedObject">
|
|
518
|
+
**Purpose:** Observe an ObservableObject owned by another view. Doesn't create or own the object.
|
|
519
|
+
|
|
520
|
+
**When to use:** Legacy code (pre-iOS 17) when receiving an ObservableObject from a parent. For iOS 17+, pass @Observable objects as plain properties.
|
|
521
|
+
|
|
522
|
+
**Ownership:** Parent or external source owns the object. This view observes it.
|
|
523
|
+
|
|
524
|
+
**Lifecycle:** Tied to the source that owns it.
|
|
525
|
+
|
|
526
|
+
```swift
|
|
527
|
+
// Legacy pattern (pre-iOS 17)
|
|
528
|
+
class SharedViewModel: ObservableObject {
|
|
529
|
+
@Published var data: String = ""
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
struct ParentView: View {
|
|
533
|
+
@StateObject private var viewModel = SharedViewModel()
|
|
534
|
+
|
|
535
|
+
var body: some View {
|
|
536
|
+
ChildView(viewModel: viewModel)
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
struct ChildView: View {
|
|
541
|
+
@ObservedObject var viewModel: SharedViewModel
|
|
542
|
+
|
|
543
|
+
var body: some View {
|
|
544
|
+
Text(viewModel.data)
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
**Common mistakes:**
|
|
550
|
+
- Creating the object within the view using @ObservedObject (use @StateObject instead, or @State for @Observable)
|
|
551
|
+
- Using for iOS 17+ code (pass @Observable objects as plain properties)
|
|
552
|
+
- Confusing ownership (if you create it, you own it - use @StateObject not @ObservedObject)
|
|
553
|
+
</wrapper>
|
|
554
|
+
</property_wrappers>
|
|
555
|
+
|
|
556
|
+
<decision_tree>
|
|
557
|
+
## Choosing the Right Property Wrapper
|
|
558
|
+
|
|
559
|
+
**iOS 17+ Decision Process:**
|
|
560
|
+
|
|
561
|
+
1. **Is this a value type (Int, String, Bool, struct)?**
|
|
562
|
+
- Owned by this view? → `@State`
|
|
563
|
+
- Passed from parent, needs modification? → `@Binding`
|
|
564
|
+
- Just reading it? → Plain property
|
|
565
|
+
|
|
566
|
+
2. **Is this an @Observable class?**
|
|
567
|
+
- Created and owned by this view? → `@State`
|
|
568
|
+
- Passed from parent, need to create bindings to properties? → `@Bindable`
|
|
569
|
+
- Passed from parent, just reading? → Plain property
|
|
570
|
+
- App-wide or subtree-wide access? → `@Environment`
|
|
571
|
+
|
|
572
|
+
3. **Is this a system value or custom environment value?**
|
|
573
|
+
→ `@Environment`
|
|
574
|
+
|
|
575
|
+
4. **Does this need to persist to UserDefaults?**
|
|
576
|
+
→ `@AppStorage`
|
|
577
|
+
|
|
578
|
+
5. **Does this need automatic state restoration per scene?**
|
|
579
|
+
→ `@SceneStorage`
|
|
580
|
+
|
|
581
|
+
**Pre-iOS 17 Decision Process:**
|
|
582
|
+
|
|
583
|
+
1. **Is this a value type?**
|
|
584
|
+
- Owned by this view? → `@State`
|
|
585
|
+
- Passed from parent? → `@Binding`
|
|
586
|
+
|
|
587
|
+
2. **Is this an ObservableObject?**
|
|
588
|
+
- Created by this view? → `@StateObject`
|
|
589
|
+
- Passed from parent? → `@ObservedObject`
|
|
590
|
+
- App-wide access? → `@EnvironmentObject`
|
|
591
|
+
|
|
592
|
+
3. **Is this a system value?**
|
|
593
|
+
→ `@Environment`
|
|
594
|
+
|
|
595
|
+
**Quick Reference Table:**
|
|
596
|
+
|
|
597
|
+
| Data Type | Ownership | iOS 17+ | Pre-iOS 17 |
|
|
598
|
+
|-----------|-----------|---------|------------|
|
|
599
|
+
| Value type | Own | @State | @State |
|
|
600
|
+
| Value type | Parent owns, need write | @Binding | @Binding |
|
|
601
|
+
| Value type | Parent owns, read only | Plain property | Plain property |
|
|
602
|
+
| @Observable class | Own | @State | N/A |
|
|
603
|
+
| @Observable class | Parent owns, need bindings | @Bindable | N/A |
|
|
604
|
+
| @Observable class | Parent owns, read only | Plain property | N/A |
|
|
605
|
+
| @Observable class | App-wide | @Environment | N/A |
|
|
606
|
+
| ObservableObject | Own | N/A | @StateObject |
|
|
607
|
+
| ObservableObject | Parent owns | N/A | @ObservedObject |
|
|
608
|
+
| ObservableObject | App-wide | N/A | @EnvironmentObject |
|
|
609
|
+
| System values | N/A | @Environment | @Environment |
|
|
610
|
+
| UserDefaults | N/A | @AppStorage | @AppStorage |
|
|
611
|
+
| State restoration | N/A | @SceneStorage | @SceneStorage |
|
|
612
|
+
</decision_tree>
|
|
613
|
+
|
|
614
|
+
<patterns>
|
|
615
|
+
## Common Patterns
|
|
616
|
+
|
|
617
|
+
<pattern name="Unidirectional Data Flow">
|
|
618
|
+
**Use when:** Building any SwiftUI view hierarchy. This is the fundamental pattern.
|
|
619
|
+
|
|
620
|
+
**Concept:** Data flows down the view hierarchy as properties. Changes flow up through bindings or callbacks. State has a single source of truth.
|
|
621
|
+
|
|
622
|
+
**Implementation:**
|
|
623
|
+
```swift
|
|
624
|
+
@Observable
|
|
625
|
+
class AppState {
|
|
626
|
+
var items: [Item] = []
|
|
627
|
+
var selectedItemId: UUID?
|
|
628
|
+
|
|
629
|
+
func selectItem(_ id: UUID) {
|
|
630
|
+
selectedItemId = id
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
func addItem(_ item: Item) {
|
|
634
|
+
items.append(item)
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
struct AppView: View {
|
|
639
|
+
@State private var appState = AppState()
|
|
640
|
+
|
|
641
|
+
var body: some View {
|
|
642
|
+
NavigationStack {
|
|
643
|
+
ItemList(
|
|
644
|
+
items: appState.items,
|
|
645
|
+
selectedId: appState.selectedItemId,
|
|
646
|
+
onSelect: { appState.selectItem($0) }
|
|
647
|
+
)
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
struct ItemList: View {
|
|
653
|
+
let items: [Item]
|
|
654
|
+
let selectedId: UUID?
|
|
655
|
+
let onSelect: (UUID) -> Void
|
|
656
|
+
|
|
657
|
+
var body: some View {
|
|
658
|
+
List(items) { item in
|
|
659
|
+
ItemRow(
|
|
660
|
+
item: item,
|
|
661
|
+
isSelected: item.id == selectedId,
|
|
662
|
+
onTap: { onSelect(item.id) }
|
|
663
|
+
)
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
struct ItemRow: View {
|
|
669
|
+
let item: Item
|
|
670
|
+
let isSelected: Bool
|
|
671
|
+
let onTap: () -> Void
|
|
672
|
+
|
|
673
|
+
var body: some View {
|
|
674
|
+
HStack {
|
|
675
|
+
Text(item.name)
|
|
676
|
+
if isSelected {
|
|
677
|
+
Image(systemName: "checkmark")
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
.onTapGesture(perform: onTap)
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
**Considerations:**
|
|
686
|
+
- Clear data flow is easier to debug than bidirectional mutations
|
|
687
|
+
- Callbacks can become verbose for deeply nested hierarchies (consider @Environment)
|
|
688
|
+
- Single source of truth prevents sync issues
|
|
689
|
+
</pattern>
|
|
690
|
+
|
|
691
|
+
<pattern name="Environment Injection">
|
|
692
|
+
**Use when:** Multiple views need access to shared state without prop drilling. Dependency injection for services.
|
|
693
|
+
|
|
694
|
+
**Implementation:**
|
|
695
|
+
```swift
|
|
696
|
+
@Observable
|
|
697
|
+
class UserSession {
|
|
698
|
+
var isLoggedIn = false
|
|
699
|
+
var username: String?
|
|
700
|
+
|
|
701
|
+
func login(username: String) {
|
|
702
|
+
self.username = username
|
|
703
|
+
isLoggedIn = true
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
func logout() {
|
|
707
|
+
username = nil
|
|
708
|
+
isLoggedIn = false
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
@main
|
|
713
|
+
struct MyApp: App {
|
|
714
|
+
@State private var session = UserSession()
|
|
715
|
+
|
|
716
|
+
var body: some Scene {
|
|
717
|
+
WindowGroup {
|
|
718
|
+
ContentView()
|
|
719
|
+
.environment(session)
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
struct ContentView: View {
|
|
725
|
+
@Environment(UserSession.self) private var session
|
|
726
|
+
|
|
727
|
+
var body: some View {
|
|
728
|
+
if session.isLoggedIn {
|
|
729
|
+
HomeView()
|
|
730
|
+
} else {
|
|
731
|
+
LoginView()
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
struct LoginView: View {
|
|
737
|
+
@Environment(UserSession.self) private var session
|
|
738
|
+
@State private var username = ""
|
|
739
|
+
|
|
740
|
+
var body: some View {
|
|
741
|
+
VStack {
|
|
742
|
+
TextField("Username", text: $username)
|
|
743
|
+
Button("Login") {
|
|
744
|
+
session.login(username: username)
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
struct HomeView: View {
|
|
751
|
+
@Environment(UserSession.self) private var session
|
|
752
|
+
|
|
753
|
+
var body: some View {
|
|
754
|
+
VStack {
|
|
755
|
+
Text("Welcome, \(session.username ?? "")")
|
|
756
|
+
Button("Logout") {
|
|
757
|
+
session.logout()
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
**Considerations:**
|
|
765
|
+
- Convenient for app-wide state (settings, auth, theme)
|
|
766
|
+
- Runtime crash if environment value not provided
|
|
767
|
+
- Can make testing harder (need to provide environment in previews/tests)
|
|
768
|
+
- Overuse can hide dependencies and make data flow unclear
|
|
769
|
+
</pattern>
|
|
770
|
+
|
|
771
|
+
<pattern name="Derived State">
|
|
772
|
+
**Use when:** Computing values from other state. Avoid storing redundant state.
|
|
773
|
+
|
|
774
|
+
**Implementation:**
|
|
775
|
+
```swift
|
|
776
|
+
@Observable
|
|
777
|
+
class ShoppingCart {
|
|
778
|
+
var items: [CartItem] = []
|
|
779
|
+
var discountCode: String?
|
|
780
|
+
|
|
781
|
+
// Derived - computed from items
|
|
782
|
+
var subtotal: Double {
|
|
783
|
+
items.reduce(0) { $0 + ($1.price * Double($1.quantity)) }
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Derived - computed from subtotal and discountCode
|
|
787
|
+
var discount: Double {
|
|
788
|
+
guard let code = discountCode else { return 0 }
|
|
789
|
+
switch code {
|
|
790
|
+
case "SAVE10": return subtotal * 0.1
|
|
791
|
+
case "SAVE20": return subtotal * 0.2
|
|
792
|
+
default: return 0
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Derived - computed from subtotal and discount
|
|
797
|
+
var total: Double {
|
|
798
|
+
subtotal - discount
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
struct CartView: View {
|
|
803
|
+
@State private var cart = ShoppingCart()
|
|
804
|
+
|
|
805
|
+
var body: some View {
|
|
806
|
+
VStack {
|
|
807
|
+
List(cart.items) { item in
|
|
808
|
+
HStack {
|
|
809
|
+
Text(item.name)
|
|
810
|
+
Spacer()
|
|
811
|
+
Text("$\(item.price * Double(item.quantity), specifier: "%.2f")")
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
Divider()
|
|
816
|
+
|
|
817
|
+
HStack {
|
|
818
|
+
Text("Subtotal:")
|
|
819
|
+
Spacer()
|
|
820
|
+
Text("$\(cart.subtotal, specifier: "%.2f")")
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if cart.discount > 0 {
|
|
824
|
+
HStack {
|
|
825
|
+
Text("Discount:")
|
|
826
|
+
Spacer()
|
|
827
|
+
Text("-$\(cart.discount, specifier: "%.2f")")
|
|
828
|
+
.foregroundStyle(.green)
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
HStack {
|
|
833
|
+
Text("Total:")
|
|
834
|
+
.bold()
|
|
835
|
+
Spacer()
|
|
836
|
+
Text("$\(cart.total, specifier: "%.2f")")
|
|
837
|
+
.bold()
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
**Considerations:**
|
|
845
|
+
- Computed properties are always in sync with source data
|
|
846
|
+
- No need to manually update derived state
|
|
847
|
+
- Recomputed on every access (cache if expensive)
|
|
848
|
+
- Keep computations simple or consider caching
|
|
849
|
+
</pattern>
|
|
850
|
+
|
|
851
|
+
<pattern name="View-Specific State">
|
|
852
|
+
**Use when:** State only matters for presentation, not business logic (UI-only state like selection, expansion, animation).
|
|
853
|
+
|
|
854
|
+
**Implementation:**
|
|
855
|
+
```swift
|
|
856
|
+
struct ExpandableCard: View {
|
|
857
|
+
let content: String
|
|
858
|
+
@State private var isExpanded = false // UI state only
|
|
859
|
+
|
|
860
|
+
var body: some View {
|
|
861
|
+
VStack(alignment: .leading) {
|
|
862
|
+
HStack {
|
|
863
|
+
Text(content.prefix(50))
|
|
864
|
+
.lineLimit(isExpanded ? nil : 1)
|
|
865
|
+
Spacer()
|
|
866
|
+
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if isExpanded {
|
|
870
|
+
Text(content)
|
|
871
|
+
.font(.caption)
|
|
872
|
+
.foregroundStyle(.secondary)
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
.padding()
|
|
876
|
+
.background(.quaternary)
|
|
877
|
+
.cornerRadius(8)
|
|
878
|
+
.onTapGesture {
|
|
879
|
+
withAnimation {
|
|
880
|
+
isExpanded.toggle()
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
**Considerations:**
|
|
888
|
+
- Keeps business logic separate from UI state
|
|
889
|
+
- Resets naturally when view is recreated
|
|
890
|
+
- Makes components self-contained and reusable
|
|
891
|
+
- Consider if state needs to persist (use @SceneStorage for restoration)
|
|
892
|
+
</pattern>
|
|
893
|
+
|
|
894
|
+
<pattern name="Sharing State Between Sibling Views">
|
|
895
|
+
**Use when:** Two sibling views need to share mutable state.
|
|
896
|
+
|
|
897
|
+
**Implementation:**
|
|
898
|
+
```swift
|
|
899
|
+
struct ParentView: View {
|
|
900
|
+
@State private var searchQuery = "" // Shared state lives in parent
|
|
901
|
+
|
|
902
|
+
var body: some View {
|
|
903
|
+
VStack {
|
|
904
|
+
SearchBar(query: $searchQuery) // Pass binding to both siblings
|
|
905
|
+
SearchResults(query: searchQuery)
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
struct SearchBar: View {
|
|
911
|
+
@Binding var query: String
|
|
912
|
+
|
|
913
|
+
var body: some View {
|
|
914
|
+
TextField("Search", text: $query)
|
|
915
|
+
.textFieldStyle(.roundedBorder)
|
|
916
|
+
.padding()
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
struct SearchResults: View {
|
|
921
|
+
let query: String
|
|
922
|
+
|
|
923
|
+
var body: some View {
|
|
924
|
+
List {
|
|
925
|
+
// Filter results based on query
|
|
926
|
+
Text("Results for: \(query)")
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
**Considerations:**
|
|
933
|
+
- State lives in lowest common ancestor
|
|
934
|
+
- Clear data flow (parent owns, children use)
|
|
935
|
+
- Siblings can't directly communicate (goes through parent)
|
|
936
|
+
- Consider @Observable model if state becomes complex
|
|
937
|
+
</pattern>
|
|
938
|
+
</patterns>
|
|
939
|
+
|
|
940
|
+
<anti_patterns>
|
|
941
|
+
## What NOT to Do
|
|
942
|
+
|
|
943
|
+
<anti_pattern name="Creating @ObservedObject in View">
|
|
944
|
+
**Problem:** Creating an ObservableObject with @ObservedObject instead of @StateObject.
|
|
945
|
+
|
|
946
|
+
**Why it's bad:** SwiftUI can recreate views at any time. @ObservedObject doesn't guarantee the object survives, causing data loss, crashes, and unpredictable behavior. The object gets recreated on every view update.
|
|
947
|
+
|
|
948
|
+
**Instead:**
|
|
949
|
+
```swift
|
|
950
|
+
// WRONG
|
|
951
|
+
struct MyView: View {
|
|
952
|
+
@ObservedObject var viewModel = ViewModel() // ❌ Will be recreated!
|
|
953
|
+
var body: some View { /* ... */ }
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// RIGHT (pre-iOS 17)
|
|
957
|
+
struct MyView: View {
|
|
958
|
+
@StateObject private var viewModel = ViewModel() // ✅ Survives redraws
|
|
959
|
+
var body: some View { /* ... */ }
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// RIGHT (iOS 17+)
|
|
963
|
+
@Observable
|
|
964
|
+
class ViewModel {
|
|
965
|
+
var data = ""
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
struct MyView: View {
|
|
969
|
+
@State private var viewModel = ViewModel() // ✅ Modern approach
|
|
970
|
+
var body: some View { /* ... */ }
|
|
971
|
+
}
|
|
972
|
+
```
|
|
973
|
+
</anti_pattern>
|
|
974
|
+
|
|
975
|
+
<anti_pattern name="Not Making @State Private">
|
|
976
|
+
**Problem:** Declaring @State properties as public or internal.
|
|
977
|
+
|
|
978
|
+
**Why it's bad:** @State is meant for view-local state. Making it public violates encapsulation and suggests the state should be passed from outside (making it not truly @State). Creates confusion about ownership.
|
|
979
|
+
|
|
980
|
+
**Instead:**
|
|
981
|
+
```swift
|
|
982
|
+
// WRONG
|
|
983
|
+
struct MyView: View {
|
|
984
|
+
@State var count = 0 // ❌ Not private
|
|
985
|
+
var body: some View { /* ... */ }
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// RIGHT
|
|
989
|
+
struct MyView: View {
|
|
990
|
+
@State private var count = 0 // ✅ Private ownership
|
|
991
|
+
var body: some View { /* ... */ }
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// If state needs to come from outside:
|
|
995
|
+
struct MyView: View {
|
|
996
|
+
@Binding var count: Int // ✅ Use @Binding instead
|
|
997
|
+
var body: some View { /* ... */ }
|
|
998
|
+
}
|
|
999
|
+
```
|
|
1000
|
+
</anti_pattern>
|
|
1001
|
+
|
|
1002
|
+
<anti_pattern name="Storing Large Objects in State">
|
|
1003
|
+
**Problem:** Storing large value types or arrays in @State, causing performance issues.
|
|
1004
|
+
|
|
1005
|
+
**Why it's bad:** SwiftUI recreates the view body whenever @State changes. Large value types cause expensive copies. Massive arrays cause performance degradation.
|
|
1006
|
+
|
|
1007
|
+
**Instead:**
|
|
1008
|
+
```swift
|
|
1009
|
+
// WRONG
|
|
1010
|
+
struct ListView: View {
|
|
1011
|
+
@State private var items: [LargeItem] = loadThousandsOfItems() // ❌ Expensive copies
|
|
1012
|
+
var body: some View { /* ... */ }
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// RIGHT
|
|
1016
|
+
@Observable
|
|
1017
|
+
class ItemStore {
|
|
1018
|
+
var items: [LargeItem] = [] // Reference type, no copies
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
struct ListView: View {
|
|
1022
|
+
@State private var store = ItemStore() // ✅ Only reference is copied
|
|
1023
|
+
var body: some View { /* ... */ }
|
|
1024
|
+
}
|
|
1025
|
+
```
|
|
1026
|
+
</anti_pattern>
|
|
1027
|
+
|
|
1028
|
+
<anti_pattern name="Using @Binding Without $">
|
|
1029
|
+
**Problem:** Passing a state value to a child expecting @Binding without the $ prefix.
|
|
1030
|
+
|
|
1031
|
+
**Why it's bad:** Passes a copy of the value instead of a binding. Child's changes don't propagate back to parent.
|
|
1032
|
+
|
|
1033
|
+
**Instead:**
|
|
1034
|
+
```swift
|
|
1035
|
+
struct ParentView: View {
|
|
1036
|
+
@State private var text = ""
|
|
1037
|
+
|
|
1038
|
+
var body: some View {
|
|
1039
|
+
// WRONG
|
|
1040
|
+
ChildView(text: text) // ❌ Passes copy
|
|
1041
|
+
|
|
1042
|
+
// RIGHT
|
|
1043
|
+
ChildView(text: $text) // ✅ Passes binding
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
struct ChildView: View {
|
|
1048
|
+
@Binding var text: String
|
|
1049
|
+
var body: some View {
|
|
1050
|
+
TextField("Enter text", text: $text)
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
```
|
|
1054
|
+
</anti_pattern>
|
|
1055
|
+
|
|
1056
|
+
<anti_pattern name="Mutating State in Computed Properties">
|
|
1057
|
+
**Problem:** Changing @State or @Observable properties inside computed properties or body.
|
|
1058
|
+
|
|
1059
|
+
**Why it's bad:** Causes infinite loops or unpredictable update cycles. SwiftUI reads body to determine what to render; mutating state during rendering triggers another render.
|
|
1060
|
+
|
|
1061
|
+
**Instead:**
|
|
1062
|
+
```swift
|
|
1063
|
+
// WRONG
|
|
1064
|
+
struct MyView: View {
|
|
1065
|
+
@State private var count = 0
|
|
1066
|
+
|
|
1067
|
+
var body: some View {
|
|
1068
|
+
let _ = count += 1 // ❌ Infinite loop!
|
|
1069
|
+
Text("Count: \(count)")
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// RIGHT
|
|
1074
|
+
struct MyView: View {
|
|
1075
|
+
@State private var count = 0
|
|
1076
|
+
|
|
1077
|
+
var body: some View {
|
|
1078
|
+
VStack {
|
|
1079
|
+
Text("Count: \(count)")
|
|
1080
|
+
Button("Increment") {
|
|
1081
|
+
count += 1 // ✅ Mutate in response to events
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
.onAppear {
|
|
1085
|
+
count = 0 // ✅ Or in lifecycle events
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
```
|
|
1090
|
+
</anti_pattern>
|
|
1091
|
+
|
|
1092
|
+
<anti_pattern name="Storing Sensitive Data in @AppStorage">
|
|
1093
|
+
**Problem:** Using @AppStorage for passwords, tokens, or other sensitive data.
|
|
1094
|
+
|
|
1095
|
+
**Why it's bad:** UserDefaults is not encrypted. Data is easily accessible to anyone with device access or backup access. Security vulnerability.
|
|
1096
|
+
|
|
1097
|
+
**Instead:**
|
|
1098
|
+
```swift
|
|
1099
|
+
// WRONG
|
|
1100
|
+
@AppStorage("password") private var password = "" // ❌ Not secure!
|
|
1101
|
+
@AppStorage("authToken") private var token = "" // ❌ Not secure!
|
|
1102
|
+
|
|
1103
|
+
// RIGHT
|
|
1104
|
+
import Security
|
|
1105
|
+
|
|
1106
|
+
class KeychainManager {
|
|
1107
|
+
func save(password: String, for account: String) {
|
|
1108
|
+
// Use Keychain for sensitive data
|
|
1109
|
+
let query: [String: Any] = [
|
|
1110
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
1111
|
+
kSecAttrAccount as String: account,
|
|
1112
|
+
kSecValueData as String: password.data(using: .utf8)!
|
|
1113
|
+
]
|
|
1114
|
+
SecItemAdd(query as CFDictionary, nil)
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// For auth tokens, user credentials, etc.
|
|
1119
|
+
struct SecureView: View {
|
|
1120
|
+
@State private var keychain = KeychainManager()
|
|
1121
|
+
|
|
1122
|
+
var body: some View {
|
|
1123
|
+
Button("Save Password") {
|
|
1124
|
+
keychain.save(password: "secret", for: "user@example.com")
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
```
|
|
1129
|
+
</anti_pattern>
|
|
1130
|
+
|
|
1131
|
+
<anti_pattern name="Using ObservableObject in iOS 17+ Code">
|
|
1132
|
+
**Problem:** Using ObservableObject, @Published, @StateObject, @ObservedObject, @EnvironmentObject in new iOS 17+ projects.
|
|
1133
|
+
|
|
1134
|
+
**Why it's bad:** The @Observable macro is simpler, more performant, and the recommended approach. Legacy patterns add unnecessary complexity. Better compiler optimization with @Observable.
|
|
1135
|
+
|
|
1136
|
+
**Instead:**
|
|
1137
|
+
```swift
|
|
1138
|
+
// WRONG (legacy)
|
|
1139
|
+
class ViewModel: ObservableObject {
|
|
1140
|
+
@Published var name = ""
|
|
1141
|
+
@Published var count = 0
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
struct OldView: View {
|
|
1145
|
+
@StateObject private var viewModel = ViewModel()
|
|
1146
|
+
var body: some View { /* ... */ }
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// RIGHT (iOS 17+)
|
|
1150
|
+
@Observable
|
|
1151
|
+
class ViewModel {
|
|
1152
|
+
var name = ""
|
|
1153
|
+
var count = 0
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
struct ModernView: View {
|
|
1157
|
+
@State private var viewModel = ViewModel()
|
|
1158
|
+
var body: some View { /* ... */ }
|
|
1159
|
+
}
|
|
1160
|
+
```
|
|
1161
|
+
</anti_pattern>
|
|
1162
|
+
|
|
1163
|
+
<anti_pattern name="Overusing Environment">
|
|
1164
|
+
**Problem:** Putting everything in @Environment, even data that should be passed as properties.
|
|
1165
|
+
|
|
1166
|
+
**Why it's bad:** Hides dependencies, makes views harder to test and preview, unclear data flow, runtime crashes if environment not provided.
|
|
1167
|
+
|
|
1168
|
+
**Instead:**
|
|
1169
|
+
```swift
|
|
1170
|
+
// WRONG - overusing environment
|
|
1171
|
+
struct ItemRow: View {
|
|
1172
|
+
@Environment(AppState.self) private var appState // ❌ Just to access one property
|
|
1173
|
+
|
|
1174
|
+
var body: some View {
|
|
1175
|
+
Text(appState.currentItem.name)
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// RIGHT - explicit dependencies
|
|
1180
|
+
struct ItemRow: View {
|
|
1181
|
+
let item: Item // ✅ Clear dependency
|
|
1182
|
+
|
|
1183
|
+
var body: some View {
|
|
1184
|
+
Text(item.name)
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Environment is good for truly cross-cutting concerns:
|
|
1189
|
+
struct ThemedView: View {
|
|
1190
|
+
@Environment(\.colorScheme) var colorScheme // ✅ System value
|
|
1191
|
+
@Environment(UserSession.self) var session // ✅ App-wide auth state
|
|
1192
|
+
|
|
1193
|
+
var body: some View { /* ... */ }
|
|
1194
|
+
}
|
|
1195
|
+
```
|
|
1196
|
+
</anti_pattern>
|
|
1197
|
+
</anti_patterns>
|
|
1198
|
+
|
|
1199
|
+
<migration_guide>
|
|
1200
|
+
## Migrating from Legacy Patterns
|
|
1201
|
+
|
|
1202
|
+
**ObservableObject → @Observable:**
|
|
1203
|
+
|
|
1204
|
+
```swift
|
|
1205
|
+
// Before (legacy)
|
|
1206
|
+
class ViewModel: ObservableObject {
|
|
1207
|
+
@Published var name: String = ""
|
|
1208
|
+
@Published var count: Int = 0
|
|
1209
|
+
@Published var items: [Item] = []
|
|
1210
|
+
|
|
1211
|
+
private var cache: [String: Any] = [:] // Not published
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
struct OldView: View {
|
|
1215
|
+
@StateObject private var viewModel = ViewModel()
|
|
1216
|
+
|
|
1217
|
+
var body: some View {
|
|
1218
|
+
Text(viewModel.name)
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// After (iOS 17+)
|
|
1223
|
+
import Observation
|
|
1224
|
+
|
|
1225
|
+
@Observable
|
|
1226
|
+
class ViewModel {
|
|
1227
|
+
var name: String = ""
|
|
1228
|
+
var count: Int = 0
|
|
1229
|
+
var items: [Item] = []
|
|
1230
|
+
|
|
1231
|
+
@ObservationIgnored
|
|
1232
|
+
private var cache: [String: Any] = [:] // Won't trigger updates
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
struct ModernView: View {
|
|
1236
|
+
@State private var viewModel = ViewModel()
|
|
1237
|
+
|
|
1238
|
+
var body: some View {
|
|
1239
|
+
Text(viewModel.name)
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
**@EnvironmentObject → @Environment:**
|
|
1245
|
+
|
|
1246
|
+
```swift
|
|
1247
|
+
// Before (legacy)
|
|
1248
|
+
class AppSettings: ObservableObject {
|
|
1249
|
+
@Published var theme: String = "light"
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
@main
|
|
1253
|
+
struct OldApp: App {
|
|
1254
|
+
@StateObject private var settings = AppSettings()
|
|
1255
|
+
|
|
1256
|
+
var body: some Scene {
|
|
1257
|
+
WindowGroup {
|
|
1258
|
+
ContentView()
|
|
1259
|
+
.environmentObject(settings)
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
struct OldContentView: View {
|
|
1265
|
+
@EnvironmentObject var settings: AppSettings
|
|
1266
|
+
|
|
1267
|
+
var body: some View {
|
|
1268
|
+
Text("Theme: \(settings.theme)")
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// After (iOS 17+)
|
|
1273
|
+
@Observable
|
|
1274
|
+
class AppSettings {
|
|
1275
|
+
var theme: String = "light"
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
@main
|
|
1279
|
+
struct ModernApp: App {
|
|
1280
|
+
@State private var settings = AppSettings()
|
|
1281
|
+
|
|
1282
|
+
var body: some Scene {
|
|
1283
|
+
WindowGroup {
|
|
1284
|
+
ContentView()
|
|
1285
|
+
.environment(settings)
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
struct ModernContentView: View {
|
|
1291
|
+
@Environment(AppSettings.self) private var settings
|
|
1292
|
+
|
|
1293
|
+
var body: some View {
|
|
1294
|
+
Text("Theme: \(settings.theme)")
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
```
|
|
1298
|
+
|
|
1299
|
+
**@ObservedObject (child views) → Plain properties:**
|
|
1300
|
+
|
|
1301
|
+
```swift
|
|
1302
|
+
// Before (legacy)
|
|
1303
|
+
class SharedData: ObservableObject {
|
|
1304
|
+
@Published var value: String = ""
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
struct ParentView: View {
|
|
1308
|
+
@StateObject private var data = SharedData()
|
|
1309
|
+
|
|
1310
|
+
var body: some View {
|
|
1311
|
+
ChildView(data: data)
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
struct ChildView: View {
|
|
1316
|
+
@ObservedObject var data: SharedData
|
|
1317
|
+
|
|
1318
|
+
var body: some View {
|
|
1319
|
+
Text(data.value)
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// After (iOS 17+)
|
|
1324
|
+
@Observable
|
|
1325
|
+
class SharedData {
|
|
1326
|
+
var value: String = ""
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
struct ParentView: View {
|
|
1330
|
+
@State private var data = SharedData()
|
|
1331
|
+
|
|
1332
|
+
var body: some View {
|
|
1333
|
+
ChildView(data: data)
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
struct ChildView: View {
|
|
1338
|
+
var data: SharedData // Plain property, no wrapper
|
|
1339
|
+
|
|
1340
|
+
var body: some View {
|
|
1341
|
+
Text(data.value)
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
```
|
|
1345
|
+
|
|
1346
|
+
**Creating bindings to @Observable properties:**
|
|
1347
|
+
|
|
1348
|
+
```swift
|
|
1349
|
+
// Before (legacy)
|
|
1350
|
+
class FormData: ObservableObject {
|
|
1351
|
+
@Published var username: String = ""
|
|
1352
|
+
@Published var email: String = ""
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
struct LegacyForm: View {
|
|
1356
|
+
@ObservedObject var formData: FormData
|
|
1357
|
+
|
|
1358
|
+
var body: some View {
|
|
1359
|
+
Form {
|
|
1360
|
+
TextField("Username", text: $formData.username)
|
|
1361
|
+
TextField("Email", text: $formData.email)
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// After (iOS 17+)
|
|
1367
|
+
@Observable
|
|
1368
|
+
class FormData {
|
|
1369
|
+
var username: String = ""
|
|
1370
|
+
var email: String = ""
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
struct ModernForm: View {
|
|
1374
|
+
@Bindable var formData: FormData
|
|
1375
|
+
|
|
1376
|
+
var body: some View {
|
|
1377
|
+
Form {
|
|
1378
|
+
TextField("Username", text: $formData.username)
|
|
1379
|
+
TextField("Email", text: $formData.email)
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
```
|
|
1384
|
+
|
|
1385
|
+
**Migration checklist:**
|
|
1386
|
+
|
|
1387
|
+
1. Add `import Observation` to files using @Observable
|
|
1388
|
+
2. Replace `class X: ObservableObject` with `@Observable class X`
|
|
1389
|
+
3. Remove `@Published` from properties (all properties are observed by default)
|
|
1390
|
+
4. Add `@ObservationIgnored` to properties that shouldn't trigger updates
|
|
1391
|
+
5. Replace `@StateObject` with `@State` in owning views
|
|
1392
|
+
6. Replace `@ObservedObject` with plain properties in child views (no wrapper)
|
|
1393
|
+
7. Replace `@EnvironmentObject` with `@Environment(Type.self)`
|
|
1394
|
+
8. Replace `.environmentObject(obj)` with `.environment(obj)`
|
|
1395
|
+
9. Use `@Bindable` when you need to create bindings to @Observable properties
|
|
1396
|
+
10. Test thoroughly - SwiftUI will warn about missing environment values at runtime
|
|
1397
|
+
</migration_guide>
|
|
1398
|
+
|
|
1399
|
+
<debugging>
|
|
1400
|
+
## Debugging State Issues
|
|
1401
|
+
|
|
1402
|
+
**State not updating views:**
|
|
1403
|
+
- Verify property is marked with correct wrapper (@State, @Observable)
|
|
1404
|
+
- Check that mutations happen on main thread for UI updates
|
|
1405
|
+
- Ensure @ObservationIgnored isn't on properties that should update views
|
|
1406
|
+
- Confirm view is actually observing the state (proper property wrapper usage)
|
|
1407
|
+
|
|
1408
|
+
**Views updating too much:**
|
|
1409
|
+
- Check if @Observable class is triggering updates from non-UI properties (use @ObservationIgnored)
|
|
1410
|
+
- Verify child views aren't receiving entire model when they only need specific properties
|
|
1411
|
+
- Consider breaking large models into smaller focused models
|
|
1412
|
+
- Use Instruments Time Profiler to identify expensive body computations
|
|
1413
|
+
|
|
1414
|
+
**Runtime crashes:**
|
|
1415
|
+
- "Missing @Environment" - Forgot to provide environment value with `.environment(value)`
|
|
1416
|
+
- Force unwrapping nil @AppStorage or @SceneStorage - Always provide default values
|
|
1417
|
+
- Access to deallocated object - Using @ObservedObject instead of @StateObject for owned objects
|
|
1418
|
+
|
|
1419
|
+
**Previews not working:**
|
|
1420
|
+
- Provide all required @Environment values in preview
|
|
1421
|
+
- Initialize @Binding properties with `.constant(value)` in previews
|
|
1422
|
+
- Ensure @Observable classes are properly initialized
|
|
1423
|
+
|
|
1424
|
+
**Example debugging view:**
|
|
1425
|
+
```swift
|
|
1426
|
+
struct DebugStateView: View {
|
|
1427
|
+
@State private var viewModel = ViewModel()
|
|
1428
|
+
|
|
1429
|
+
var body: some View {
|
|
1430
|
+
VStack {
|
|
1431
|
+
Text("Count: \(viewModel.count)")
|
|
1432
|
+
Button("Increment") {
|
|
1433
|
+
print("Before: \(viewModel.count)")
|
|
1434
|
+
viewModel.count += 1
|
|
1435
|
+
print("After: \(viewModel.count)")
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
// Add debugging modifier
|
|
1439
|
+
._printChanges() // Prints when view updates and why
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
```
|
|
1443
|
+
</debugging>
|