@aicgen/aicgen 1.0.0-beta.1 → 1.0.0
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/{.claude/guidelines → .agent/rules}/api-design.md +5 -1
- package/{.claude/guidelines → .agent/rules}/architecture.md +5 -1
- package/{.claude/guidelines → .agent/rules}/best-practices.md +5 -1
- package/{.claude/guidelines → .agent/rules}/code-style.md +5 -1
- package/{.claude/guidelines → .agent/rules}/design-patterns.md +5 -1
- package/{.claude/guidelines → .agent/rules}/devops.md +5 -1
- package/{.claude/guidelines → .agent/rules}/error-handling.md +5 -1
- package/.agent/rules/instructions.md +28 -0
- package/{.claude/guidelines → .agent/rules}/language.md +5 -1
- package/{.claude/guidelines → .agent/rules}/performance.md +5 -1
- package/{.claude/guidelines → .agent/rules}/security.md +5 -1
- package/{.claude/guidelines → .agent/rules}/testing.md +5 -1
- package/.agent/workflows/add-documentation.md +10 -0
- package/.agent/workflows/generate-integration-tests.md +10 -0
- package/.agent/workflows/generate-unit-tests.md +11 -0
- package/.agent/workflows/performance-audit.md +11 -0
- package/.agent/workflows/refactor-extract-module.md +12 -0
- package/.agent/workflows/security-audit.md +12 -0
- package/.gemini/instructions.md +4843 -0
- package/.vs/ProjectSettings.json +2 -2
- package/.vs/VSWorkspaceState.json +15 -15
- package/.vs/aicgen.slnx/v18/DocumentLayout.json +53 -53
- package/AGENTS.md +9 -11
- package/assets/icon.svg +33 -33
- package/bun.lock +734 -26
- package/{CLAUDE.md → claude.md} +2 -2
- package/config.example.yml +129 -0
- package/config.yml +38 -0
- package/data/architecture/microservices/api-gateway.md +56 -56
- package/data/devops/observability.md +73 -73
- package/data/guideline-mappings.yml +128 -0
- package/data/language/dart/async.md +289 -0
- package/data/language/dart/basics.md +280 -0
- package/data/language/dart/error-handling.md +355 -0
- package/data/language/dart/index.md +10 -0
- package/data/language/dart/testing.md +352 -0
- package/data/language/swift/basics.md +477 -0
- package/data/language/swift/concurrency.md +654 -0
- package/data/language/swift/error-handling.md +679 -0
- package/data/language/swift/swiftui-mvvm.md +795 -0
- package/data/language/swift/testing.md +708 -0
- package/data/version.json +10 -8
- package/dist/index.js +50153 -28959
- package/jest.config.js +46 -0
- package/package.json +14 -3
- package/.claude/agents/architecture-reviewer.md +0 -88
- package/.claude/agents/guideline-checker.md +0 -73
- package/.claude/agents/security-auditor.md +0 -108
- package/.claude/settings.json +0 -98
- package/.claude/settings.local.json +0 -8
- package/.eslintrc.json +0 -28
- package/.github/workflows/release.yml +0 -180
- package/.github/workflows/test.yml +0 -81
- package/CONTRIBUTING.md +0 -821
- package/dist/commands/init.d.ts +0 -8
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -46
- package/dist/commands/init.js.map +0 -1
- package/dist/config/profiles.d.ts +0 -4
- package/dist/config/profiles.d.ts.map +0 -1
- package/dist/config/profiles.js +0 -30
- package/dist/config/profiles.js.map +0 -1
- package/dist/config/settings.d.ts +0 -7
- package/dist/config/settings.d.ts.map +0 -1
- package/dist/config/settings.js +0 -7
- package/dist/config/settings.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/models/guideline.d.ts +0 -15
- package/dist/models/guideline.d.ts.map +0 -1
- package/dist/models/guideline.js +0 -2
- package/dist/models/guideline.js.map +0 -1
- package/dist/models/preference.d.ts +0 -9
- package/dist/models/preference.d.ts.map +0 -1
- package/dist/models/preference.js +0 -2
- package/dist/models/preference.js.map +0 -1
- package/dist/models/profile.d.ts +0 -9
- package/dist/models/profile.d.ts.map +0 -1
- package/dist/models/profile.js +0 -2
- package/dist/models/profile.js.map +0 -1
- package/dist/models/project.d.ts +0 -13
- package/dist/models/project.d.ts.map +0 -1
- package/dist/models/project.js +0 -2
- package/dist/models/project.js.map +0 -1
- package/dist/services/ai/anthropic.d.ts +0 -7
- package/dist/services/ai/anthropic.d.ts.map +0 -1
- package/dist/services/ai/anthropic.js +0 -39
- package/dist/services/ai/anthropic.js.map +0 -1
- package/dist/services/generator.d.ts +0 -2
- package/dist/services/generator.d.ts.map +0 -1
- package/dist/services/generator.js +0 -4
- package/dist/services/generator.js.map +0 -1
- package/dist/services/learner.d.ts +0 -2
- package/dist/services/learner.d.ts.map +0 -1
- package/dist/services/learner.js +0 -4
- package/dist/services/learner.js.map +0 -1
- package/dist/services/scanner.d.ts +0 -3
- package/dist/services/scanner.d.ts.map +0 -1
- package/dist/services/scanner.js +0 -54
- package/dist/services/scanner.js.map +0 -1
- package/dist/utils/errors.d.ts +0 -15
- package/dist/utils/errors.d.ts.map +0 -1
- package/dist/utils/errors.js +0 -27
- package/dist/utils/errors.js.map +0 -1
- package/dist/utils/file.d.ts +0 -7
- package/dist/utils/file.d.ts.map +0 -1
- package/dist/utils/file.js +0 -32
- package/dist/utils/file.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -6
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -17
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/path.d.ts +0 -6
- package/dist/utils/path.d.ts.map +0 -1
- package/dist/utils/path.js +0 -14
- package/dist/utils/path.js.map +0 -1
- package/docs/planning/memory-lane.md +0 -83
- package/packaging/linux/aicgen.spec +0 -23
- package/packaging/linux/control +0 -9
- package/packaging/macos/scripts/postinstall +0 -12
- package/packaging/windows/setup.nsi +0 -92
- package/scripts/add-categories.ts +0 -87
- package/scripts/build-binary.ts +0 -46
- package/scripts/embed-data.ts +0 -105
- package/scripts/generate-version.ts +0 -150
- package/scripts/test-decompress.ts +0 -27
- package/scripts/test-extract.ts +0 -31
- package/src/__tests__/services/assistant-file-writer.test.ts +0 -400
- package/src/__tests__/services/guideline-loader.test.ts +0 -281
- package/src/__tests__/services/tarball-extraction.test.ts +0 -125
- package/src/commands/add-guideline.ts +0 -296
- package/src/commands/clear.ts +0 -61
- package/src/commands/guideline-selector.ts +0 -123
- package/src/commands/init.ts +0 -645
- package/src/commands/quick-add.ts +0 -586
- package/src/commands/remove-guideline.ts +0 -152
- package/src/commands/stats.ts +0 -49
- package/src/commands/update.ts +0 -240
- package/src/config.ts +0 -82
- package/src/embedded-data.ts +0 -1492
- package/src/index.ts +0 -67
- package/src/models/profile.ts +0 -24
- package/src/models/project.ts +0 -43
- package/src/services/assistant-file-writer.ts +0 -612
- package/src/services/config-generator.ts +0 -150
- package/src/services/config-manager.ts +0 -70
- package/src/services/data-source.ts +0 -248
- package/src/services/first-run-init.ts +0 -148
- package/src/services/guideline-loader.ts +0 -311
- package/src/services/hook-generator.ts +0 -178
- package/src/services/subagent-generator.ts +0 -310
- package/src/utils/banner.ts +0 -66
- package/src/utils/errors.ts +0 -27
- package/src/utils/file.ts +0 -67
- package/src/utils/formatting.ts +0 -172
- package/src/utils/logger.ts +0 -89
- package/src/utils/path.ts +0 -17
- package/src/utils/wizard-state.ts +0 -132
- package/tsconfig.json +0 -25
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
# SwiftUI and MVVM Architecture
|
|
2
|
+
|
|
3
|
+
## MVVM Pattern in SwiftUI
|
|
4
|
+
|
|
5
|
+
SwiftUI works naturally with the MVVM (Model-View-ViewModel) pattern:
|
|
6
|
+
|
|
7
|
+
```swift
|
|
8
|
+
// Model - Data and business logic
|
|
9
|
+
struct User: Identifiable, Codable {
|
|
10
|
+
let id: String
|
|
11
|
+
var name: String
|
|
12
|
+
var email: String
|
|
13
|
+
var isActive: Bool
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ViewModel - Presentation logic
|
|
17
|
+
@MainActor
|
|
18
|
+
class UserViewModel: ObservableObject {
|
|
19
|
+
@Published var users: [User] = []
|
|
20
|
+
@Published var isLoading = false
|
|
21
|
+
@Published var errorMessage: String?
|
|
22
|
+
|
|
23
|
+
private let repository: UserRepository
|
|
24
|
+
|
|
25
|
+
init(repository: UserRepository = UserRepository()) {
|
|
26
|
+
self.repository = repository
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func loadUsers() async {
|
|
30
|
+
isLoading = true
|
|
31
|
+
errorMessage = nil
|
|
32
|
+
|
|
33
|
+
do {
|
|
34
|
+
users = try await repository.fetchUsers()
|
|
35
|
+
} catch {
|
|
36
|
+
errorMessage = "Failed to load users: \(error.localizedDescription)"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
isLoading = false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func deleteUser(_ user: User) async {
|
|
43
|
+
do {
|
|
44
|
+
try await repository.delete(user)
|
|
45
|
+
users.removeAll { $0.id == user.id }
|
|
46
|
+
} catch {
|
|
47
|
+
errorMessage = "Failed to delete user: \(error.localizedDescription)"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// View - UI and user interaction
|
|
53
|
+
struct UserListView: View {
|
|
54
|
+
@StateObject private var viewModel = UserViewModel()
|
|
55
|
+
|
|
56
|
+
var body: some View {
|
|
57
|
+
NavigationView {
|
|
58
|
+
Group {
|
|
59
|
+
if viewModel.isLoading {
|
|
60
|
+
ProgressView("Loading users...")
|
|
61
|
+
} else if let error = viewModel.errorMessage {
|
|
62
|
+
ErrorView(message: error) {
|
|
63
|
+
Task { await viewModel.loadUsers() }
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
userList
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
.navigationTitle("Users")
|
|
70
|
+
.task {
|
|
71
|
+
await viewModel.loadUsers()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private var userList: some View {
|
|
77
|
+
List {
|
|
78
|
+
ForEach(viewModel.users) { user in
|
|
79
|
+
UserRow(user: user)
|
|
80
|
+
}
|
|
81
|
+
.onDelete { indexSet in
|
|
82
|
+
Task {
|
|
83
|
+
for index in indexSet {
|
|
84
|
+
await viewModel.deleteUser(viewModel.users[index])
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## State Management
|
|
94
|
+
|
|
95
|
+
### @State for View-Local State
|
|
96
|
+
|
|
97
|
+
```swift
|
|
98
|
+
// ✅ Use @State for simple view-specific state
|
|
99
|
+
struct CounterView: View {
|
|
100
|
+
@State private var count = 0
|
|
101
|
+
|
|
102
|
+
var body: some View {
|
|
103
|
+
VStack {
|
|
104
|
+
Text("Count: \(count)")
|
|
105
|
+
Button("Increment") {
|
|
106
|
+
count += 1
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ❌ Don't use @State for complex or shared state
|
|
113
|
+
struct BadView: View {
|
|
114
|
+
@State private var users: [User] = [] // Should be in ViewModel
|
|
115
|
+
@State private var repository = UserRepository() // Should be injected
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### @StateObject for ViewModel Ownership
|
|
120
|
+
|
|
121
|
+
```swift
|
|
122
|
+
// ✅ Use @StateObject when view creates and owns the ViewModel
|
|
123
|
+
struct UserListView: View {
|
|
124
|
+
@StateObject private var viewModel = UserViewModel()
|
|
125
|
+
|
|
126
|
+
var body: some View {
|
|
127
|
+
List(viewModel.users) { user in
|
|
128
|
+
Text(user.name)
|
|
129
|
+
}
|
|
130
|
+
.task {
|
|
131
|
+
await viewModel.loadUsers()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ✅ StateObject with dependency injection
|
|
137
|
+
struct UserListView: View {
|
|
138
|
+
@StateObject private var viewModel: UserViewModel
|
|
139
|
+
|
|
140
|
+
init(repository: UserRepository) {
|
|
141
|
+
_viewModel = StateObject(wrappedValue: UserViewModel(repository: repository))
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### @ObservedObject for Passed ViewModels
|
|
147
|
+
|
|
148
|
+
```swift
|
|
149
|
+
// ✅ Use @ObservedObject when ViewModel is passed from parent
|
|
150
|
+
struct UserDetailView: View {
|
|
151
|
+
@ObservedObject var viewModel: UserDetailViewModel
|
|
152
|
+
|
|
153
|
+
var body: some View {
|
|
154
|
+
VStack {
|
|
155
|
+
Text(viewModel.user.name)
|
|
156
|
+
TextField("Name", text: $viewModel.editableName)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Parent view creates and passes ViewModel
|
|
162
|
+
struct ParentView: View {
|
|
163
|
+
@StateObject private var detailViewModel = UserDetailViewModel()
|
|
164
|
+
|
|
165
|
+
var body: some View {
|
|
166
|
+
UserDetailView(viewModel: detailViewModel)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### @EnvironmentObject for App-Wide State
|
|
172
|
+
|
|
173
|
+
```swift
|
|
174
|
+
// ✅ Use @EnvironmentObject for app-wide dependencies
|
|
175
|
+
class AppState: ObservableObject {
|
|
176
|
+
@Published var currentUser: User?
|
|
177
|
+
@Published var isAuthenticated = false
|
|
178
|
+
|
|
179
|
+
func login(user: User) {
|
|
180
|
+
currentUser = user
|
|
181
|
+
isAuthenticated = true
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
func logout() {
|
|
185
|
+
currentUser = nil
|
|
186
|
+
isAuthenticated = false
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Inject at app level
|
|
191
|
+
@main
|
|
192
|
+
struct MyApp: App {
|
|
193
|
+
@StateObject private var appState = AppState()
|
|
194
|
+
|
|
195
|
+
var body: some Scene {
|
|
196
|
+
WindowGroup {
|
|
197
|
+
ContentView()
|
|
198
|
+
.environmentObject(appState)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Access in any view
|
|
204
|
+
struct ProfileView: View {
|
|
205
|
+
@EnvironmentObject var appState: AppState
|
|
206
|
+
|
|
207
|
+
var body: some View {
|
|
208
|
+
if let user = appState.currentUser {
|
|
209
|
+
Text("Welcome, \(user.name)")
|
|
210
|
+
Button("Logout") {
|
|
211
|
+
appState.logout()
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### @Binding for Two-Way Data Flow
|
|
219
|
+
|
|
220
|
+
```swift
|
|
221
|
+
// ✅ Use @Binding for child views that need to modify parent state
|
|
222
|
+
struct ToggleRow: View {
|
|
223
|
+
let title: String
|
|
224
|
+
@Binding var isOn: Bool
|
|
225
|
+
|
|
226
|
+
var body: some View {
|
|
227
|
+
Toggle(title, isOn: $isOn)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Parent provides the binding
|
|
232
|
+
struct SettingsView: View {
|
|
233
|
+
@State private var notificationsEnabled = false
|
|
234
|
+
|
|
235
|
+
var body: some View {
|
|
236
|
+
ToggleRow(title: "Notifications", isOn: $notificationsEnabled)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## View Composition
|
|
242
|
+
|
|
243
|
+
### Extract Subviews
|
|
244
|
+
|
|
245
|
+
```swift
|
|
246
|
+
// ❌ Monolithic view
|
|
247
|
+
struct ProductView: View {
|
|
248
|
+
let product: Product
|
|
249
|
+
|
|
250
|
+
var body: some View {
|
|
251
|
+
VStack {
|
|
252
|
+
HStack {
|
|
253
|
+
AsyncImage(url: product.imageURL) { image in
|
|
254
|
+
image.resizable().aspectRatio(contentMode: .fit)
|
|
255
|
+
} placeholder: {
|
|
256
|
+
ProgressView()
|
|
257
|
+
}
|
|
258
|
+
.frame(width: 60, height: 60)
|
|
259
|
+
|
|
260
|
+
VStack(alignment: .leading) {
|
|
261
|
+
Text(product.name).font(.headline)
|
|
262
|
+
Text(product.description).font(.subheadline).foregroundColor(.gray)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
HStack {
|
|
267
|
+
Text("$\(product.price, specifier: "%.2f")")
|
|
268
|
+
Spacer()
|
|
269
|
+
if product.inStock {
|
|
270
|
+
Text("In Stock").foregroundColor(.green)
|
|
271
|
+
} else {
|
|
272
|
+
Text("Out of Stock").foregroundColor(.red)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ✅ Composed from smaller views
|
|
280
|
+
struct ProductView: View {
|
|
281
|
+
let product: Product
|
|
282
|
+
|
|
283
|
+
var body: some View {
|
|
284
|
+
VStack(alignment: .leading, spacing: 12) {
|
|
285
|
+
ProductHeader(product: product)
|
|
286
|
+
ProductFooter(product: product)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
struct ProductHeader: View {
|
|
292
|
+
let product: Product
|
|
293
|
+
|
|
294
|
+
var body: some View {
|
|
295
|
+
HStack {
|
|
296
|
+
ProductImage(url: product.imageURL)
|
|
297
|
+
ProductInfo(name: product.name, description: product.description)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
struct ProductFooter: View {
|
|
303
|
+
let product: Product
|
|
304
|
+
|
|
305
|
+
var body: some View {
|
|
306
|
+
HStack {
|
|
307
|
+
PriceLabel(price: product.price)
|
|
308
|
+
Spacer()
|
|
309
|
+
StockStatus(inStock: product.inStock)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Use ViewBuilder
|
|
316
|
+
|
|
317
|
+
```swift
|
|
318
|
+
// ✅ ViewBuilder for conditional content
|
|
319
|
+
struct ContentView: View {
|
|
320
|
+
let isLoggedIn: Bool
|
|
321
|
+
|
|
322
|
+
var body: some View {
|
|
323
|
+
VStack {
|
|
324
|
+
headerContent
|
|
325
|
+
mainContent
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
@ViewBuilder
|
|
330
|
+
private var headerContent: some View {
|
|
331
|
+
if isLoggedIn {
|
|
332
|
+
ProfileHeader()
|
|
333
|
+
} else {
|
|
334
|
+
LoginPrompt()
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
@ViewBuilder
|
|
339
|
+
private var mainContent: some View {
|
|
340
|
+
Text("Main Content")
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Navigation Patterns
|
|
346
|
+
|
|
347
|
+
### NavigationStack (iOS 16+)
|
|
348
|
+
|
|
349
|
+
```swift
|
|
350
|
+
// ✅ Type-safe navigation with NavigationStack
|
|
351
|
+
struct NavigationExample: View {
|
|
352
|
+
@State private var path = NavigationPath()
|
|
353
|
+
|
|
354
|
+
var body: some View {
|
|
355
|
+
NavigationStack(path: $path) {
|
|
356
|
+
List(users) { user in
|
|
357
|
+
NavigationLink(value: user) {
|
|
358
|
+
UserRow(user: user)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
.navigationDestination(for: User.self) { user in
|
|
362
|
+
UserDetailView(user: user)
|
|
363
|
+
}
|
|
364
|
+
.navigationTitle("Users")
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ✅ Programmatic navigation
|
|
370
|
+
struct ProgrammaticNav: View {
|
|
371
|
+
@State private var path = NavigationPath()
|
|
372
|
+
|
|
373
|
+
var body: some View {
|
|
374
|
+
NavigationStack(path: $path) {
|
|
375
|
+
Button("Go to Detail") {
|
|
376
|
+
path.append(DetailRoute.userDetail(id: "123"))
|
|
377
|
+
}
|
|
378
|
+
.navigationDestination(for: DetailRoute.self) { route in
|
|
379
|
+
destinationView(for: route)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
@ViewBuilder
|
|
385
|
+
func destinationView(for route: DetailRoute) -> some View {
|
|
386
|
+
switch route {
|
|
387
|
+
case .userDetail(let id):
|
|
388
|
+
UserDetailView(userId: id)
|
|
389
|
+
case .settings:
|
|
390
|
+
SettingsView()
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
enum DetailRoute: Hashable {
|
|
396
|
+
case userDetail(id: String)
|
|
397
|
+
case settings
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Sheet Presentation
|
|
402
|
+
|
|
403
|
+
```swift
|
|
404
|
+
// ✅ Present sheet with binding
|
|
405
|
+
struct SheetExample: View {
|
|
406
|
+
@State private var showingSheet = false
|
|
407
|
+
@State private var selectedUser: User?
|
|
408
|
+
|
|
409
|
+
var body: some View {
|
|
410
|
+
List(users) { user in
|
|
411
|
+
Button(user.name) {
|
|
412
|
+
selectedUser = user
|
|
413
|
+
showingSheet = true
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
.sheet(isPresented: $showingSheet) {
|
|
417
|
+
if let user = selectedUser {
|
|
418
|
+
UserDetailView(user: user)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ✅ Use item-based sheet (cleaner)
|
|
425
|
+
struct ItemSheetExample: View {
|
|
426
|
+
@State private var selectedUser: User?
|
|
427
|
+
|
|
428
|
+
var body: some View {
|
|
429
|
+
List(users) { user in
|
|
430
|
+
Button(user.name) {
|
|
431
|
+
selectedUser = user
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
.sheet(item: $selectedUser) { user in
|
|
435
|
+
UserDetailView(user: user)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
## Data Flow Patterns
|
|
442
|
+
|
|
443
|
+
### Unidirectional Data Flow
|
|
444
|
+
|
|
445
|
+
```swift
|
|
446
|
+
// ✅ Data flows down, events flow up
|
|
447
|
+
struct ParentView: View {
|
|
448
|
+
@StateObject private var viewModel = ParentViewModel()
|
|
449
|
+
|
|
450
|
+
var body: some View {
|
|
451
|
+
VStack {
|
|
452
|
+
// Data flows down
|
|
453
|
+
ChildView(
|
|
454
|
+
data: viewModel.data,
|
|
455
|
+
// Events flow up
|
|
456
|
+
onAction: { action in
|
|
457
|
+
viewModel.handle(action)
|
|
458
|
+
}
|
|
459
|
+
)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
struct ChildView: View {
|
|
465
|
+
let data: String
|
|
466
|
+
let onAction: (Action) -> Void
|
|
467
|
+
|
|
468
|
+
var body: some View {
|
|
469
|
+
Button(data) {
|
|
470
|
+
onAction(.buttonTapped) // Event flows up
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
enum Action {
|
|
476
|
+
case buttonTapped
|
|
477
|
+
case itemSelected(id: String)
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Combine Integration
|
|
482
|
+
|
|
483
|
+
```swift
|
|
484
|
+
// ✅ Use Combine for reactive updates
|
|
485
|
+
@MainActor
|
|
486
|
+
class SearchViewModel: ObservableObject {
|
|
487
|
+
@Published var searchText = ""
|
|
488
|
+
@Published var results: [SearchResult] = []
|
|
489
|
+
@Published var isSearching = false
|
|
490
|
+
|
|
491
|
+
private var cancellables = Set<AnyCancellable>()
|
|
492
|
+
private let searchService: SearchService
|
|
493
|
+
|
|
494
|
+
init(searchService: SearchService = SearchService()) {
|
|
495
|
+
self.searchService = searchService
|
|
496
|
+
|
|
497
|
+
$searchText
|
|
498
|
+
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
|
|
499
|
+
.removeDuplicates()
|
|
500
|
+
.sink { [weak self] query in
|
|
501
|
+
Task {
|
|
502
|
+
await self?.search(query: query)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
.store(in: &cancellables)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private func search(query: String) async {
|
|
509
|
+
guard !query.isEmpty else {
|
|
510
|
+
results = []
|
|
511
|
+
return
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
isSearching = true
|
|
515
|
+
defer { isSearching = false }
|
|
516
|
+
|
|
517
|
+
do {
|
|
518
|
+
results = try await searchService.search(query: query)
|
|
519
|
+
} catch {
|
|
520
|
+
results = []
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
## Custom View Modifiers
|
|
527
|
+
|
|
528
|
+
```swift
|
|
529
|
+
// ✅ Reusable styling with ViewModifier
|
|
530
|
+
struct CardStyle: ViewModifier {
|
|
531
|
+
func body(content: Content) -> some View {
|
|
532
|
+
content
|
|
533
|
+
.padding()
|
|
534
|
+
.background(Color.white)
|
|
535
|
+
.cornerRadius(12)
|
|
536
|
+
.shadow(radius: 4)
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
extension View {
|
|
541
|
+
func cardStyle() -> some View {
|
|
542
|
+
modifier(CardStyle())
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Usage
|
|
547
|
+
Text("Hello")
|
|
548
|
+
.cardStyle()
|
|
549
|
+
|
|
550
|
+
// ✅ Parameterized modifiers
|
|
551
|
+
struct PrimaryButtonStyle: ViewModifier {
|
|
552
|
+
let isEnabled: Bool
|
|
553
|
+
|
|
554
|
+
func body(content: Content) -> some View {
|
|
555
|
+
content
|
|
556
|
+
.font(.headline)
|
|
557
|
+
.foregroundColor(.white)
|
|
558
|
+
.padding()
|
|
559
|
+
.background(isEnabled ? Color.blue : Color.gray)
|
|
560
|
+
.cornerRadius(8)
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
extension View {
|
|
565
|
+
func primaryButton(enabled: Bool = true) -> some View {
|
|
566
|
+
modifier(PrimaryButtonStyle(isEnabled: enabled))
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
## Performance Optimization
|
|
572
|
+
|
|
573
|
+
### Avoid Expensive Computations in body
|
|
574
|
+
|
|
575
|
+
```swift
|
|
576
|
+
// ❌ Expensive work in body (called on every update)
|
|
577
|
+
struct BadView: View {
|
|
578
|
+
let items: [Item]
|
|
579
|
+
|
|
580
|
+
var body: some View {
|
|
581
|
+
let processed = processExpensiveData(items) // Recomputed every render!
|
|
582
|
+
List(processed) { item in
|
|
583
|
+
Text(item.name)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ✅ Compute in ViewModel or use computed property
|
|
589
|
+
@MainActor
|
|
590
|
+
class GoodViewModel: ObservableObject {
|
|
591
|
+
@Published var items: [Item] = []
|
|
592
|
+
|
|
593
|
+
var processedItems: [ProcessedItem] {
|
|
594
|
+
processExpensiveData(items)
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ✅ Or memoize the result
|
|
599
|
+
struct GoodView: View {
|
|
600
|
+
let items: [Item]
|
|
601
|
+
|
|
602
|
+
private var processedItems: [ProcessedItem] {
|
|
603
|
+
// Only recomputed when items change
|
|
604
|
+
items.map { ProcessedItem($0) }
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
var body: some View {
|
|
608
|
+
List(processedItems) { item in
|
|
609
|
+
Text(item.name)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### Use Equatable for Performance
|
|
616
|
+
|
|
617
|
+
```swift
|
|
618
|
+
// ✅ Conform to Equatable to avoid unnecessary updates
|
|
619
|
+
struct UserRow: View, Equatable {
|
|
620
|
+
let user: User
|
|
621
|
+
|
|
622
|
+
var body: some View {
|
|
623
|
+
HStack {
|
|
624
|
+
AsyncImage(url: user.avatarURL)
|
|
625
|
+
Text(user.name)
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
static func == (lhs: UserRow, rhs: UserRow) -> Bool {
|
|
630
|
+
lhs.user.id == rhs.user.id
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Use .equatable() to enable optimization
|
|
635
|
+
List(users) { user in
|
|
636
|
+
UserRow(user: user)
|
|
637
|
+
.equatable()
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
## Testing ViewModels
|
|
642
|
+
|
|
643
|
+
```swift
|
|
644
|
+
// ✅ Testable ViewModel with dependency injection
|
|
645
|
+
@MainActor
|
|
646
|
+
class UserViewModel: ObservableObject {
|
|
647
|
+
@Published var users: [User] = []
|
|
648
|
+
private let repository: UserRepositoryProtocol
|
|
649
|
+
|
|
650
|
+
init(repository: UserRepositoryProtocol) {
|
|
651
|
+
self.repository = repository
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
func loadUsers() async {
|
|
655
|
+
users = try? await repository.fetchUsers()
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Test with mock repository
|
|
660
|
+
@MainActor
|
|
661
|
+
class UserViewModelTests: XCTestCase {
|
|
662
|
+
func testLoadUsers() async {
|
|
663
|
+
let mockRepo = MockUserRepository()
|
|
664
|
+
mockRepo.usersToReturn = [
|
|
665
|
+
User(id: "1", name: "Alice"),
|
|
666
|
+
User(id: "2", name: "Bob")
|
|
667
|
+
]
|
|
668
|
+
|
|
669
|
+
let viewModel = UserViewModel(repository: mockRepo)
|
|
670
|
+
await viewModel.loadUsers()
|
|
671
|
+
|
|
672
|
+
XCTAssertEqual(viewModel.users.count, 2)
|
|
673
|
+
XCTAssertEqual(viewModel.users.first?.name, "Alice")
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
## Best Practices
|
|
679
|
+
|
|
680
|
+
### Keep Views Simple
|
|
681
|
+
|
|
682
|
+
```swift
|
|
683
|
+
// ✅ View only contains UI logic
|
|
684
|
+
struct ProductView: View {
|
|
685
|
+
@ObservedObject var viewModel: ProductViewModel
|
|
686
|
+
|
|
687
|
+
var body: some View {
|
|
688
|
+
VStack {
|
|
689
|
+
Text(viewModel.productName)
|
|
690
|
+
Text(viewModel.formattedPrice)
|
|
691
|
+
Button("Add to Cart") {
|
|
692
|
+
Task { await viewModel.addToCart() }
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// ViewModel contains all business logic
|
|
699
|
+
@MainActor
|
|
700
|
+
class ProductViewModel: ObservableObject {
|
|
701
|
+
@Published var product: Product
|
|
702
|
+
|
|
703
|
+
var productName: String { product.name }
|
|
704
|
+
var formattedPrice: String { "$\(product.price, specifier: "%.2f")" }
|
|
705
|
+
|
|
706
|
+
func addToCart() async {
|
|
707
|
+
// Business logic here
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
### Use Preview Provider
|
|
713
|
+
|
|
714
|
+
```swift
|
|
715
|
+
// ✅ Provide previews for development
|
|
716
|
+
struct UserListView_Previews: PreviewProvider {
|
|
717
|
+
static var previews: some View {
|
|
718
|
+
Group {
|
|
719
|
+
// Default state
|
|
720
|
+
UserListView()
|
|
721
|
+
.previewDisplayName("Default")
|
|
722
|
+
|
|
723
|
+
// Loading state
|
|
724
|
+
UserListView(viewModel: .loading)
|
|
725
|
+
.previewDisplayName("Loading")
|
|
726
|
+
|
|
727
|
+
// Error state
|
|
728
|
+
UserListView(viewModel: .error)
|
|
729
|
+
.previewDisplayName("Error")
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Helper to create preview ViewModels
|
|
735
|
+
extension UserViewModel {
|
|
736
|
+
static var loading: UserViewModel {
|
|
737
|
+
let vm = UserViewModel()
|
|
738
|
+
vm.isLoading = true
|
|
739
|
+
return vm
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
static var error: UserViewModel {
|
|
743
|
+
let vm = UserViewModel()
|
|
744
|
+
vm.errorMessage = "Failed to load users"
|
|
745
|
+
return vm
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
### Environment for Dependencies
|
|
751
|
+
|
|
752
|
+
```swift
|
|
753
|
+
// ✅ Use Environment for injecting dependencies
|
|
754
|
+
private struct RepositoryKey: EnvironmentKey {
|
|
755
|
+
static let defaultValue: UserRepository = UserRepository()
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
extension EnvironmentValues {
|
|
759
|
+
var userRepository: UserRepository {
|
|
760
|
+
get { self[RepositoryKey.self] }
|
|
761
|
+
set { self[RepositoryKey.self] = newValue }
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Provide at app level
|
|
766
|
+
@main
|
|
767
|
+
struct MyApp: App {
|
|
768
|
+
let repository = UserRepository()
|
|
769
|
+
|
|
770
|
+
var body: some Scene {
|
|
771
|
+
WindowGroup {
|
|
772
|
+
ContentView()
|
|
773
|
+
.environment(\.userRepository, repository)
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Access in views
|
|
779
|
+
struct UserListView: View {
|
|
780
|
+
@Environment(\.userRepository) var repository
|
|
781
|
+
@StateObject private var viewModel: UserViewModel
|
|
782
|
+
|
|
783
|
+
init() {
|
|
784
|
+
_viewModel = StateObject(wrappedValue: UserViewModel(repository: repository))
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
---
|
|
790
|
+
|
|
791
|
+
**Sources:**
|
|
792
|
+
- [Apple SwiftUI Documentation](https://developer.apple.com/documentation/swiftui/)
|
|
793
|
+
- [SwiftUI Data Flow](https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app)
|
|
794
|
+
- [WWDC22: The SwiftUI cookbook for navigation](https://developer.apple.com/videos/play/wwdc2022/10054/)
|
|
795
|
+
- [Swift by Sundell: SwiftUI Architecture](https://www.swiftbysundell.com/basics/swiftui/)
|