swift-code-reviewer-skill 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -874,6 +874,734 @@ struct LoginView_Previews: PreviewProvider { // ❌ Old pattern
874
874
 
875
875
  ---
876
876
 
877
+ ## 8. Navigation Architecture
878
+
879
+ ### 8.1 Route Enum Enforcement
880
+
881
+ **Check for:**
882
+ - [ ] Navigation destinations defined as typed `Hashable` enums (not String/Int/raw values)
883
+ - [ ] Route enum covers all destinations in the stack
884
+ - [ ] Associated values carry only the necessary data (IDs, not full models when possible)
885
+
886
+ **Examples:**
887
+
888
+ ❌ **Bad: String-based navigation**
889
+ ```swift
890
+ struct ContentView: View {
891
+ @State private var path: [String] = [] // ❌ Stringly-typed route
892
+
893
+ var body: some View {
894
+ NavigationStack(path: $path) {
895
+ List(items) { item in
896
+ NavigationLink(value: "detail-\(item.id)") { // ❌ Magic string
897
+ Text(item.name)
898
+ }
899
+ }
900
+ }
901
+ }
902
+ }
903
+ ```
904
+
905
+ ✅ **Good: Typed route enum**
906
+ ```swift
907
+ enum AppRoute: Hashable {
908
+ case userDetail(userID: UUID)
909
+ case userEdit(userID: UUID)
910
+ case settings
911
+ case notifications
912
+ }
913
+
914
+ struct ContentView: View {
915
+ @State private var router = RouterPath()
916
+
917
+ var body: some View {
918
+ NavigationStack(path: $router.path) {
919
+ List(users) { user in
920
+ NavigationLink(value: AppRoute.userDetail(userID: user.id)) {
921
+ Text(user.name)
922
+ }
923
+ }
924
+ .navigationDestination(for: AppRoute.self) { route in
925
+ routeDestination(route)
926
+ }
927
+ }
928
+ }
929
+ }
930
+ ```
931
+
932
+ **Reference**: `~/.claude/skills/swiftui-ui-patterns/references/navigation.md`
933
+
934
+ ### 8.2 RouterPath Pattern
935
+
936
+ **Check for:**
937
+ - [ ] Navigation path owned by `RouterPath` `@Observable` class, not ad-hoc `@State var path`
938
+ - [ ] RouterPath exposed as `@Observable` so views can bind to it
939
+ - [ ] Path manipulation methods (`navigate(to:)`, `pop()`, `popToRoot()`) on RouterPath
940
+
941
+ **Examples:**
942
+
943
+ ❌ **Bad: Ad-hoc @State path**
944
+ ```swift
945
+ struct RootView: View {
946
+ @State private var path = NavigationPath() // ❌ Ad-hoc, not reusable
947
+ @State private var showProfile = false
948
+
949
+ var body: some View {
950
+ NavigationStack(path: $path) {
951
+ // ...
952
+ }
953
+ }
954
+ }
955
+ ```
956
+
957
+ ✅ **Good: RouterPath @Observable**
958
+ ```swift
959
+ @Observable
960
+ final class RouterPath {
961
+ var path: [AppRoute] = []
962
+
963
+ func navigate(to route: AppRoute) {
964
+ path.append(route)
965
+ }
966
+
967
+ func pop() {
968
+ guard !path.isEmpty else { return }
969
+ path.removeLast()
970
+ }
971
+
972
+ func popToRoot() {
973
+ path.removeAll()
974
+ }
975
+
976
+ func navigate(to routes: [AppRoute]) {
977
+ path.append(contentsOf: routes)
978
+ }
979
+ }
980
+
981
+ struct RootView: View {
982
+ @State private var router = RouterPath()
983
+
984
+ var body: some View {
985
+ NavigationStack(path: $router.path) {
986
+ HomeView(router: router)
987
+ .navigationDestination(for: AppRoute.self) { route in
988
+ destinationView(for: route, router: router)
989
+ }
990
+ }
991
+ }
992
+ }
993
+ ```
994
+
995
+ ### 8.3 Centralized navigationDestination
996
+
997
+ **Check for:**
998
+ - [ ] Single `.navigationDestination(for: Route.self)` per NavigationStack (not scattered in child views)
999
+ - [ ] Destination mapping in root or coordinator view
1000
+ - [ ] No `.navigationDestination` in deeply nested views for top-level routes
1001
+
1002
+ **Examples:**
1003
+
1004
+ ❌ **Bad: Scattered navigationDestination**
1005
+ ```swift
1006
+ struct HomeView: View {
1007
+ var body: some View {
1008
+ List(items) { item in
1009
+ NavigationLink(value: AppRoute.userDetail(userID: item.id)) {
1010
+ Text(item.name)
1011
+ }
1012
+ }
1013
+ .navigationDestination(for: AppRoute.self) { route in // ❌ Defined in child
1014
+ // Only handles some routes
1015
+ }
1016
+ }
1017
+ }
1018
+ ```
1019
+
1020
+ ✅ **Good: Centralized in root**
1021
+ ```swift
1022
+ struct RootView: View {
1023
+ @State private var router = RouterPath()
1024
+
1025
+ var body: some View {
1026
+ NavigationStack(path: $router.path) {
1027
+ HomeView(router: router)
1028
+ .navigationDestination(for: AppRoute.self) { route in // ✅ Single, centralized
1029
+ switch route {
1030
+ case .userDetail(let id): UserDetailView(userID: id, router: router)
1031
+ case .userEdit(let id): UserEditView(userID: id, router: router)
1032
+ case .settings: SettingsView(router: router)
1033
+ case .notifications: NotificationsView(router: router)
1034
+ }
1035
+ }
1036
+ }
1037
+ .environment(router)
1038
+ }
1039
+ }
1040
+ ```
1041
+
1042
+ ---
1043
+
1044
+ ## 9. Sheet / Modal Routing
1045
+
1046
+ ### 9.1 Item-Driven Sheet
1047
+
1048
+ **Check for:**
1049
+ - [ ] `.sheet(item:)` preferred over `.sheet(isPresented:)` when a model is being selected/shown
1050
+ - [ ] Sheet dismissed by setting item to `nil` (not manual boolean reset)
1051
+ - [ ] No manual boolean reset after sheet dismiss
1052
+
1053
+ **Examples:**
1054
+
1055
+ ❌ **Bad: Boolean-driven sheet with manual item**
1056
+ ```swift
1057
+ struct UserListView: View {
1058
+ @State private var showDetail = false // ❌ Manual boolean
1059
+ @State private var selectedUser: User? // ❌ Two states to keep in sync
1060
+
1061
+ var body: some View {
1062
+ List(users) { user in
1063
+ Button { selectedUser = user; showDetail = true } label: { Text(user.name) }
1064
+ }
1065
+ .sheet(isPresented: $showDetail) {
1066
+ if let user = selectedUser { // ❌ Forced optional unwrap in sheet
1067
+ UserDetailSheet(user: user)
1068
+ }
1069
+ }
1070
+ }
1071
+ }
1072
+ ```
1073
+
1074
+ ✅ **Good: Item-driven sheet**
1075
+ ```swift
1076
+ struct UserListView: View {
1077
+ @State private var selectedUser: User? // ✅ Single source of truth
1078
+
1079
+ var body: some View {
1080
+ List(users) { user in
1081
+ Button { selectedUser = user } label: { Text(user.name) }
1082
+ }
1083
+ .sheet(item: $selectedUser) { user in // ✅ Item-driven, auto-dismisses on nil
1084
+ UserDetailSheet(user: user)
1085
+ }
1086
+ }
1087
+ }
1088
+ ```
1089
+
1090
+ ### 9.2 SheetDestination Enum
1091
+
1092
+ **Check for:**
1093
+ - [ ] Multiple sheets represented as a single `Identifiable` enum, not multiple `@State` booleans
1094
+ - [ ] `SheetDestination` enum covers all possible modals in the view
1095
+ - [ ] Only one `.sheet(item:)` call per view
1096
+
1097
+ **Examples:**
1098
+
1099
+ ❌ **Bad: Multiple boolean states for sheets**
1100
+ ```swift
1101
+ struct HomeView: View {
1102
+ @State private var showCompose = false // ❌ Multiple booleans
1103
+ @State private var showProfile = false // ❌ Multiple booleans
1104
+ @State private var showSettings = false // ❌ Multiple booleans
1105
+
1106
+ var body: some View {
1107
+ // ...
1108
+ .sheet(isPresented: $showCompose) { ComposeView() }
1109
+ .sheet(isPresented: $showProfile) { ProfileView() } // ❌ Multiple .sheet on same view
1110
+ .sheet(isPresented: $showSettings) { SettingsView() }
1111
+ }
1112
+ }
1113
+ ```
1114
+
1115
+ ✅ **Good: SheetDestination enum**
1116
+ ```swift
1117
+ enum SheetDestination: Identifiable {
1118
+ case compose
1119
+ case profile(userID: UUID)
1120
+ case settings
1121
+
1122
+ var id: String {
1123
+ switch self {
1124
+ case .compose: return "compose"
1125
+ case .profile(let id): return "profile-\(id)"
1126
+ case .settings: return "settings"
1127
+ }
1128
+ }
1129
+ }
1130
+
1131
+ struct HomeView: View {
1132
+ @State private var sheetDestination: SheetDestination? // ✅ Single state
1133
+
1134
+ var body: some View {
1135
+ // ...
1136
+ .sheet(item: $sheetDestination) { destination in // ✅ Single .sheet
1137
+ switch destination {
1138
+ case .compose: ComposeView()
1139
+ case .profile(let id): ProfileView(userID: id)
1140
+ case .settings: SettingsView()
1141
+ }
1142
+ }
1143
+ }
1144
+ }
1145
+ ```
1146
+
1147
+ ---
1148
+
1149
+ ## 10. Deep Link Handling
1150
+
1151
+ ### 10.1 Centralized Deep Link Routing
1152
+
1153
+ **Check for:**
1154
+ - [ ] `.onOpenURL` applied at the app root (not in feature views)
1155
+ - [ ] URL parsing and validation happens in a dedicated router/coordinator
1156
+ - [ ] Feature views do not contain URL parsing logic
1157
+
1158
+ **Examples:**
1159
+
1160
+ ❌ **Bad: onOpenURL scattered in feature views**
1161
+ ```swift
1162
+ struct HomeView: View {
1163
+ var body: some View {
1164
+ // ...
1165
+ .onOpenURL { url in // ❌ URL handling in feature view
1166
+ if url.pathComponents.contains("profile") {
1167
+ // Handle profile deep link
1168
+ }
1169
+ }
1170
+ }
1171
+ }
1172
+
1173
+ struct SettingsView: View {
1174
+ var body: some View {
1175
+ // ...
1176
+ .onOpenURL { url in // ❌ Another scattered handler
1177
+ if url.pathComponents.contains("settings") {
1178
+ // Handle settings deep link
1179
+ }
1180
+ }
1181
+ }
1182
+ }
1183
+ ```
1184
+
1185
+ ✅ **Good: Centralized at root, router handles routing**
1186
+ ```swift
1187
+ @Observable
1188
+ final class RouterPath {
1189
+ var path: [AppRoute] = []
1190
+
1191
+ func handle(url: URL) -> Bool {
1192
+ guard let route = AppRoute(url: url) else { return false } // ✅ URL → Route
1193
+ navigate(to: route)
1194
+ return true
1195
+ }
1196
+ }
1197
+
1198
+ // In App root
1199
+ struct RootView: View {
1200
+ @State private var router = RouterPath()
1201
+
1202
+ var body: some View {
1203
+ NavigationStack(path: $router.path) {
1204
+ HomeView()
1205
+ .navigationDestination(for: AppRoute.self) { route in
1206
+ destinationView(for: route)
1207
+ }
1208
+ }
1209
+ .onOpenURL { url in // ✅ Single handler at root
1210
+ _ = router.handle(url: url)
1211
+ }
1212
+ }
1213
+ }
1214
+ ```
1215
+
1216
+ ---
1217
+
1218
+ ## 11. TabView Architecture
1219
+
1220
+ ### 11.1 Independent Navigation History Per Tab
1221
+
1222
+ **Check for:**
1223
+ - [ ] Each tab has its own `RouterPath` (not a shared global path)
1224
+ - [ ] Switching tabs preserves the navigation stack for each tab
1225
+ - [ ] Tab routers are independent `@Observable` instances
1226
+
1227
+ **Examples:**
1228
+
1229
+ ❌ **Bad: Shared navigation path across tabs**
1230
+ ```swift
1231
+ struct MainTabView: View {
1232
+ @State private var path = NavigationPath() // ❌ Shared across all tabs
1233
+
1234
+ var body: some View {
1235
+ TabView {
1236
+ NavigationStack(path: $path) { HomeView() }.tabItem { Label("Home", systemImage: "house") }
1237
+ NavigationStack(path: $path) { SearchView() }.tabItem { Label("Search", systemImage: "magnifyingglass") }
1238
+ }
1239
+ }
1240
+ }
1241
+ ```
1242
+
1243
+ ✅ **Good: Independent RouterPath per tab**
1244
+ ```swift
1245
+ enum AppTab: Int, CaseIterable {
1246
+ case home, search, notifications, profile
1247
+ }
1248
+
1249
+ @Observable
1250
+ final class AppTabRouter {
1251
+ var selectedTab: AppTab = .home
1252
+
1253
+ // Independent path per tab
1254
+ var homeRouter = RouterPath()
1255
+ var searchRouter = RouterPath()
1256
+ var notificationsRouter = RouterPath()
1257
+ var profileRouter = RouterPath()
1258
+
1259
+ func router(for tab: AppTab) -> RouterPath {
1260
+ switch tab {
1261
+ case .home: return homeRouter
1262
+ case .search: return searchRouter
1263
+ case .notifications: return notificationsRouter
1264
+ case .profile: return profileRouter
1265
+ }
1266
+ }
1267
+ }
1268
+
1269
+ struct MainTabView: View {
1270
+ @State private var tabRouter = AppTabRouter()
1271
+
1272
+ var body: some View {
1273
+ TabView(selection: $tabRouter.selectedTab) {
1274
+ Tab("Home", systemImage: "house", value: .home) {
1275
+ NavigationStack(path: $tabRouter.homeRouter.path) { // ✅ Per-tab router
1276
+ HomeView(router: tabRouter.homeRouter)
1277
+ }
1278
+ }
1279
+ Tab("Search", systemImage: "magnifyingglass", value: .search) {
1280
+ NavigationStack(path: $tabRouter.searchRouter.path) { // ✅ Per-tab router
1281
+ SearchView(router: tabRouter.searchRouter)
1282
+ }
1283
+ }
1284
+ }
1285
+ }
1286
+ }
1287
+ ```
1288
+
1289
+ ### 11.2 Custom Tab Binding with Side Effects
1290
+
1291
+ **Check for:**
1292
+ - [ ] Action tabs (e.g., compose, post) handled as side effects, not actual tab destinations
1293
+ - [ ] `AppTab` enum distinguishes navigable tabs from action tabs
1294
+ - [ ] Tab selection goes through `updateTab(_:)` or equivalent to handle action tabs
1295
+
1296
+ **Examples:**
1297
+
1298
+ ❌ **Bad: Action tab treated as normal tab**
1299
+ ```swift
1300
+ struct MainTabView: View {
1301
+ @State private var selectedTab = 0
1302
+
1303
+ var body: some View {
1304
+ TabView(selection: $selectedTab) {
1305
+ HomeView().tabItem { Label("Home", systemImage: "house") }.tag(0)
1306
+ EmptyView().tabItem { Label("Compose", systemImage: "plus") }.tag(1) // ❌ No action
1307
+ ProfileView().tabItem { Label("Profile", systemImage: "person") }.tag(2)
1308
+ }
1309
+ }
1310
+ }
1311
+ ```
1312
+
1313
+ ✅ **Good: Action tabs trigger side effects**
1314
+ ```swift
1315
+ enum AppTab: Int {
1316
+ case home, compose, profile
1317
+
1318
+ var isAction: Bool { self == .compose }
1319
+ }
1320
+
1321
+ @Observable
1322
+ final class AppTabRouter {
1323
+ var selectedTab: AppTab = .home
1324
+ var showCompose = false
1325
+
1326
+ func updateTab(_ tab: AppTab) {
1327
+ if tab.isAction {
1328
+ showCompose = true // ✅ Action tab triggers modal, not navigation
1329
+ } else {
1330
+ selectedTab = tab
1331
+ }
1332
+ }
1333
+ }
1334
+
1335
+ struct MainTabView: View {
1336
+ @State private var tabRouter = AppTabRouter()
1337
+
1338
+ var body: some View {
1339
+ TabView(selection: Binding(
1340
+ get: { tabRouter.selectedTab },
1341
+ set: { tabRouter.updateTab($0) } // ✅ Route through updateTab
1342
+ )) {
1343
+ HomeView().tabItem { Label("Home", systemImage: "house") }.tag(AppTab.home)
1344
+ Color.clear.tabItem { Label("Compose", systemImage: "plus") }.tag(AppTab.compose)
1345
+ ProfileView().tabItem { Label("Profile", systemImage: "person") }.tag(AppTab.profile)
1346
+ }
1347
+ .sheet(isPresented: $tabRouter.showCompose) {
1348
+ ComposeView()
1349
+ }
1350
+ }
1351
+ }
1352
+ ```
1353
+
1354
+ ---
1355
+
1356
+ ## 12. Theming Enforcement
1357
+
1358
+ ### 12.1 Semantic Colors via Theme Object
1359
+
1360
+ **Check for:**
1361
+ - [ ] No raw color values (`Color.blue`, `Color.white`, `Color(hex:)`) when a `Theme` object exists
1362
+ - [ ] Colors accessed via `@Environment(Theme.self)` or equivalent design token
1363
+ - [ ] Theme propagated at app root via `.environment(theme)`
1364
+
1365
+ **Examples:**
1366
+
1367
+ ❌ **Bad: Raw color values**
1368
+ ```swift
1369
+ struct PostRowView: View {
1370
+ let post: Post
1371
+
1372
+ var body: some View {
1373
+ HStack {
1374
+ Text(post.author)
1375
+ .foregroundStyle(Color.gray) // ❌ Raw color
1376
+ Text(post.content)
1377
+ .foregroundStyle(Color.black) // ❌ Raw color
1378
+ Spacer()
1379
+ Image(systemName: "heart")
1380
+ .foregroundStyle(Color.red) // ❌ Raw color
1381
+ }
1382
+ .background(Color.white) // ❌ Raw color
1383
+ }
1384
+ }
1385
+ ```
1386
+
1387
+ ✅ **Good: Semantic colors via Theme**
1388
+ ```swift
1389
+ struct PostRowView: View {
1390
+ let post: Post
1391
+ @Environment(Theme.self) private var theme // ✅ Theme from environment
1392
+
1393
+ var body: some View {
1394
+ HStack {
1395
+ Text(post.author)
1396
+ .foregroundStyle(theme.labelSecondary) // ✅ Semantic color
1397
+ Text(post.content)
1398
+ .foregroundStyle(theme.labelPrimary) // ✅ Semantic color
1399
+ Spacer()
1400
+ Image(systemName: "heart")
1401
+ .foregroundStyle(theme.tintColor) // ✅ Semantic color
1402
+ }
1403
+ .background(theme.primaryBackground) // ✅ Semantic color
1404
+ }
1405
+ }
1406
+
1407
+ // Provide at app root
1408
+ @main
1409
+ struct MyApp: App {
1410
+ @State private var theme = Theme.default
1411
+
1412
+ var body: some Scene {
1413
+ WindowGroup {
1414
+ ContentView()
1415
+ .environment(theme) // ✅ Theme propagated to all views
1416
+ }
1417
+ }
1418
+ }
1419
+ ```
1420
+
1421
+ ---
1422
+
1423
+ ## 13. Async State Patterns
1424
+
1425
+ ### 13.1 .task(id:) for Input-Driven Work
1426
+
1427
+ **Check for:**
1428
+ - [ ] `.task(id: someValue)` used instead of `.onChange + Task { }` for input-driven async work
1429
+ - [ ] `CancellationError` silenced (not re-thrown or shown as error to user)
1430
+ - [ ] Debounce implemented inside `.task(id:)` using `Task.sleep` before the actual work
1431
+
1432
+ **Examples:**
1433
+
1434
+ ❌ **Bad: .onChange + manual Task**
1435
+ ```swift
1436
+ struct SearchView: View {
1437
+ @State private var query = ""
1438
+ @State private var results: [Result] = []
1439
+
1440
+ var body: some View {
1441
+ TextField("Search", text: $query)
1442
+ List(results) { result in ResultRow(result: result) }
1443
+ .onChange(of: query) { _, newValue in
1444
+ Task { // ❌ Manual Task, previous not cancelled properly
1445
+ results = await search(query: newValue)
1446
+ }
1447
+ }
1448
+ }
1449
+ }
1450
+ ```
1451
+
1452
+ ✅ **Good: .task(id:) with built-in cancellation and debounce**
1453
+ ```swift
1454
+ struct SearchView: View {
1455
+ @State private var query = ""
1456
+ @State private var results: [SearchResult] = []
1457
+
1458
+ var body: some View {
1459
+ TextField("Search", text: $query)
1460
+ List(results) { result in ResultRow(result: result) }
1461
+ .task(id: query) { // ✅ Auto-cancels previous task when query changes
1462
+ do {
1463
+ try await Task.sleep(for: .milliseconds(300)) // ✅ Debounce inside task
1464
+ results = try await search(query: query)
1465
+ } catch is CancellationError {
1466
+ // ✅ Silenced — expected when task is superseded
1467
+ } catch {
1468
+ // Handle real errors
1469
+ }
1470
+ }
1471
+ }
1472
+ }
1473
+ ```
1474
+
1475
+ ### 13.2 Explicit Loading/Error States
1476
+
1477
+ **Check for:**
1478
+ - [ ] `LoadState<T>` enum (or equivalent) used instead of multiple booleans (`isLoading`, `hasError`, `isEmpty`)
1479
+ - [ ] All states represented: `.idle`, `.loading`, `.loaded(T)`, `.error(Error)`
1480
+ - [ ] View switches on `LoadState` to render appropriate UI
1481
+
1482
+ **Examples:**
1483
+
1484
+ ❌ **Bad: Multiple boolean flags**
1485
+ ```swift
1486
+ @Observable
1487
+ final class UserListViewModel {
1488
+ var users: [User] = []
1489
+ var isLoading: Bool = false // ❌ Multiple flags
1490
+ var hasError: Bool = false // ❌ Multiple flags
1491
+ var errorMessage: String = "" // ❌ Multiple flags
1492
+ var isEmpty: Bool = false // ❌ Derived, should be computed
1493
+ }
1494
+ ```
1495
+
1496
+ ✅ **Good: LoadState enum**
1497
+ ```swift
1498
+ enum LoadState<T> {
1499
+ case idle
1500
+ case loading
1501
+ case loaded(T)
1502
+ case error(Error)
1503
+ }
1504
+
1505
+ @Observable
1506
+ final class UserListViewModel {
1507
+ var loadState: LoadState<[User]> = .idle // ✅ Single source of truth
1508
+
1509
+ func loadUsers() async {
1510
+ loadState = .loading
1511
+ do {
1512
+ let users = try await userRepository.fetchUsers()
1513
+ loadState = .loaded(users)
1514
+ } catch {
1515
+ loadState = .error(error)
1516
+ }
1517
+ }
1518
+ }
1519
+
1520
+ struct UserListView: View {
1521
+ let viewModel: UserListViewModel
1522
+
1523
+ var body: some View {
1524
+ Group {
1525
+ switch viewModel.loadState {
1526
+ case .idle: EmptyView()
1527
+ case .loading: ProgressView()
1528
+ case .loaded(let users): UserListContent(users: users)
1529
+ case .error(let error): ErrorView(error: error, retry: { Task { await viewModel.loadUsers() } })
1530
+ }
1531
+ }
1532
+ .task { await viewModel.loadUsers() }
1533
+ }
1534
+ }
1535
+ ```
1536
+
1537
+ ---
1538
+
1539
+ ## 14. Focus and Input Patterns
1540
+
1541
+ ### 14.1 Focus State Chaining
1542
+
1543
+ **Check for:**
1544
+ - [ ] `FocusField` enum used with `@FocusState` instead of multiple boolean focus states
1545
+ - [ ] `.onSubmit` advances focus to the next field in the sequence
1546
+ - [ ] Last field in chain submits the form or triggers the primary action
1547
+
1548
+ **Examples:**
1549
+
1550
+ ❌ **Bad: Multiple boolean focus states**
1551
+ ```swift
1552
+ struct LoginFormView: View {
1553
+ @State private var email = ""
1554
+ @State private var password = ""
1555
+ @FocusState private var emailFocused: Bool // ❌ Separate boolean per field
1556
+ @FocusState private var passwordFocused: Bool // ❌ Separate boolean per field
1557
+
1558
+ var body: some View {
1559
+ TextField("Email", text: $email).focused($emailFocused)
1560
+ SecureField("Password", text: $password).focused($passwordFocused)
1561
+ }
1562
+ }
1563
+ ```
1564
+
1565
+ ✅ **Good: FocusField enum with chaining**
1566
+ ```swift
1567
+ struct LoginFormView: View {
1568
+ @State private var email = ""
1569
+ @State private var password = ""
1570
+ @FocusState private var focusedField: FocusField? // ✅ Single enum for all fields
1571
+
1572
+ enum FocusField {
1573
+ case email, password
1574
+ }
1575
+
1576
+ var body: some View {
1577
+ VStack(spacing: 16) {
1578
+ TextField("Email", text: $email)
1579
+ .focused($focusedField, equals: .email)
1580
+ .textContentType(.emailAddress)
1581
+ .keyboardType(.emailAddress)
1582
+ .submitLabel(.next)
1583
+ .onSubmit { focusedField = .password } // ✅ Chain to next field
1584
+
1585
+ SecureField("Password", text: $password)
1586
+ .focused($focusedField, equals: .password)
1587
+ .textContentType(.password)
1588
+ .submitLabel(.done)
1589
+ .onSubmit { login() } // ✅ Last field submits form
1590
+
1591
+ Button("Log In", action: login)
1592
+ }
1593
+ .onAppear { focusedField = .email } // ✅ Auto-focus first field
1594
+ }
1595
+
1596
+ private func login() {
1597
+ focusedField = nil // ✅ Dismiss keyboard on submit
1598
+ Task { await viewModel.login() }
1599
+ }
1600
+ }
1601
+ ```
1602
+
1603
+ ---
1604
+
877
1605
  ## Quick Reference Checklist
878
1606
 
879
1607
  ### Critical Issues
@@ -888,6 +1616,12 @@ struct LoginView_Previews: PreviewProvider { // ❌ Old pattern
888
1616
  - [ ] .task instead of .onAppear for async work (iOS 15+)
889
1617
  - [ ] Proper property wrapper selection
890
1618
  - [ ] View extraction for complex views
1619
+ - [ ] Route destinations as typed Hashable enum (not String/Int raw values)
1620
+ - [ ] RouterPath @Observable owns navigation path (not ad-hoc @State)
1621
+ - [ ] `.sheet(item:)` preferred when model is selected
1622
+ - [ ] Multiple sheets use SheetDestination enum (not multiple booleans)
1623
+ - [ ] Independent RouterPath per tab (not shared path)
1624
+ - [ ] `.task(id:)` for input-driven async work with CancellationError silenced
891
1625
 
892
1626
  ### Medium Priority
893
1627
  - [ ] Modern .onChange syntax (iOS 17+)
@@ -895,6 +1629,10 @@ struct LoginView_Previews: PreviewProvider { // ❌ Old pattern
895
1629
  - [ ] Dynamic Type support
896
1630
  - [ ] Equatable conformance for view models
897
1631
  - [ ] #Preview macro (iOS 17+)
1632
+ - [ ] Semantic colors via Theme @Environment (no raw Color.blue/Color.white)
1633
+ - [ ] FocusField enum with @FocusState for multi-field forms
1634
+ - [ ] `.onOpenURL` at app root (not in feature views)
1635
+ - [ ] LoadState<T> enum instead of multiple isLoading/hasError booleans
898
1636
 
899
1637
  ### Low Priority
900
1638
  - [ ] View body < 50 lines