You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
-**Then Performance (Priority 2).** Minimize thread hopping. Swift 6.2 defaults to staying on the current actor. Only offload heavy CPU tasks to the global pool explicitly using `@concurrent` functions or background actors.
20
20
-**Safety over convenience.** Swift optionals protect against null pointers. Avoid force-unwrapping optionals (`!`) or force-casting types (`as!`). Use safe bindings (`if let`, `guard let`) and supply fallback default values.
21
21
-**Cooperative Thread Scheduling.** Do not block threads with synchronous loops or heavy operations. Yield often inside loops, or isolate intensive calculations to dedicated background actors.
22
+
-**Data flows down, events flow up.** Keep a single source of truth. Avoid child views mutating parent state directly; hoist state or pass explicit `@Binding`/callbacks.
22
23
23
24
## Memory Safety & Code Integrity
24
25
-**Reference Management:** Always use `weak` or `unowned` references inside closures to prevent reference cycles and memory leaks. Use the Xcode memory graph or profiling tools to audit lifecycle boundaries.
@@ -50,6 +51,8 @@ Always choose the concurrency model corresponding to the workload:
50
51
3.**Structured Cancel Check:** Ensure long-running loops check for task cancellation using `Task.checkCancellation()` or checking `Task.isCancelled`.
51
52
4.**Zipped Arguments:** In parameter testing, use `zip()` to align test arguments instead of Cartesian products.
52
53
5.**Thread Sanitizer:** Run tests with Thread Sanitizer (TSan) active in Xcode Diagnostics to catch runtime race conditions.
54
+
6.**Lifecycle-Scoped Tasking:** Use `.task` or `.task(id:)` instead of `onAppear` + `Task {}` to tie async operations to the view lifecycle.
55
+
7.**Identity Stability:** Prefer stable `Identifiable` elements in loops (`ForEach`/`List`) over indices to avoid layout and state rendering issues.
53
56
54
57
## Build, Tooling & CI (Non-Negotiable)
55
58
-**Toolchain floor:** Swift 6.2, Xcode 26+.
@@ -64,6 +67,9 @@ Always choose the concurrency model corresponding to the workload:
64
67
- Updating UI-bound properties from background threads (always isolate ViewModels to `@MainActor`).
65
68
- Spawning detached unstructured tasks in hot loops.
66
69
- Mixing legacy `XCTestCase` and modern `Testing` frameworks in the same test targets.
70
+
- Performing expensive sorting, formatting, or image decoding inside `body`.
71
+
- Using `.indices` for dynamic `ForEach`/`List` loops.
72
+
- Child views mutating shared/parent state via unchecked environment objects instead of using explicit bindings or callback closures.
67
73
68
74
## Pre-Commit Checklist (Verify Every Time)
69
75
-[ ] Concurrency checking set to Strict Mode; warnings treated as errors
@@ -75,9 +81,13 @@ Always choose the concurrency model corresponding to the workload:
75
81
-[ ] Test suites use the new Swift Testing `@Suite` and `@Test` macros
76
82
-[ ] Parameterized tests use `arguments` and `zip` to keep inputs aligned
77
83
-[ ] All tests execute and pass cleanly under Thread Sanitizer (TSan) active
84
+
-[ ] List views and `ForEach` utilize stable `Identifiable` objects instead of `.indices`
85
+
-[ ] Expensive view operations (e.g. formatters, complex parsing) are computed outside of `body`
86
+
-[ ] Subviews communicate changes back via `@Binding` or event callback closures
87
+
-[ ] Async actions in views are tied to `.task` or `.task(id:)` for automatic lifetime management
78
88
79
89
## References & Further Reading
80
-
- Load `references/Spacecraft_Swift_Guidelines.md` for full skeletons (Swift 6.2 concurrent offloader, MainActor ViewModel, Zipped parameterized test, and optional binding) when deeper patterns are needed.
90
+
- Load `references/Spacecraft_Swift_Guidelines.md` for full skeletons (Swift 6.2 concurrent offloader, MainActor ViewModel, Zipped parameterized test, optional binding, SwiftUI state composition, list performance, lifecycle tasking, and API migration charts) when deeper patterns are needed.
81
91
-*Further reading* (consulted for background only): Swift Concurrency Proposals (SE-0461, SE-0470), SwiftUI Performance Audits, Swift Testing Guide, and Apple Security Guidelines.
82
92
83
93
When the user requests Swift code or review, activate this skill, apply the checklist, and produce code a senior Spacecraft systems engineer would ship.
Copy file name to clipboardExpand all lines: spacecraft-swift-guidelines/references/Spacecraft_Swift_Guidelines.md
+191Lines changed: 191 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -279,3 +279,194 @@ Before merging Swift code, verify:
279
279
3. Every asynchronous closure capturing `self` uses the `[weak self]` capture list.
280
280
4. ViewModels update state variables on the `@MainActor`.
281
281
5. Unit tests run on the modern `Testing` framework (not `XCTest`).
282
+
283
+
---
284
+
285
+
## 8. SwiftUI State Ownership & View Composition
286
+
287
+
Ensure every view uses the correct property wrapper based on ownership, and keep data flow unidirectional: data flows down, events flow up.
288
+
289
+
### Property Wrapper Decision Tree
290
+
1.**Ephemeral Local Value Type:** View owns the state and it should reset when the view is destroyed -> Use `@State`.
291
+
2.**Parent-Owned Value Type:** Parent view owns the state and this view only reads and writes to it -> Use `@Binding`.
292
+
3.**Reference-Type Model (iOS 17+):** Model has complex business logic/derived state -> Use `@Observable` macro on the class, and reference it directly in the view.
293
+
4.**Reference-Type Model (Legacy / Backwards Compatibility):** Model supports iOS 16- -> Use `@StateObject` in the owner view, and `@ObservedObject` in child views.
294
+
5.**Shared App-Wide/Feature-Wide State:** State is shared across many child views in the hierarchy -> Inject via `@Environment` (for `@Observable` models) or `@EnvironmentObject` (for legacy `ObservableObject`).
295
+
296
+
### Skeleton: Parent-Child Unidirectional Data Flow
To avoid layout churn, rendering lags, and state loss, lists and loop containers must use stable and unique identity.
371
+
372
+
### Guidelines
373
+
-**Always use `Identifiable` objects** in `ForEach` and `List` containers.
374
+
-**Never use `.indices` or `id: \.self` on mutable/dynamic arrays.** If items shift (inserts, deletes), index-based identity breaks SwiftUI's diffing engine, causing row churn and animation glitching.
375
+
-**Choose Container Wisely:** Use `List` for standard system features (selection, swipe-to-delete, refreshable). Use `ScrollView` + `LazyVStack` (with `LazyVStack` wrapping ONLY the repeating items) for fully custom layouts.
376
+
377
+
### Skeleton: Performant List with Stable Identity
SwiftUI views should tie async tasks to their lifecycle using `.task` to automatically cancel active background tasks when the view disappears.
409
+
410
+
### Lifecycle Comparison
411
+
-**View-scoped loading:** Use `.task { await loadData() }`.
412
+
-**State-driven reload:** Use `.task(id: filterID) { await loadFilteredData() }`. Cancels the current task and restarts it when the ID changes.
413
+
-**Avoid:**`onAppear { Task { ... } }` since tasks spawned this way escape the view's lifecycle and will continue running in the background even if the user navigates away, leading to leaks and stale updates.
414
+
415
+
### Skeleton: Lifecycle-Scoped Async Task
416
+
```swift
417
+
importSwiftUI
418
+
419
+
structLogMonitorView: View {
420
+
let sourceURL: URL
421
+
@Stateprivatevar logs: [String] = []
422
+
@Stateprivatevar isRefreshing =false
423
+
424
+
var body: some View {
425
+
List(logs, id: \.self) { log in
426
+
Text(log)
427
+
}
428
+
// Runs when view appears, automatically cancelled when view disappears
429
+
.task {
430
+
awaitfetchLogs()
431
+
}
432
+
// Automatically cancels and restarts if sourceURL changes
433
+
.task(id: sourceURL) {
434
+
awaitfetchLogs()
435
+
}
436
+
}
437
+
438
+
privatefuncfetchLogs() async {
439
+
do {
440
+
let (data, _) =tryawait URLSession.shared.data(from: sourceURL)
|`NavigationView`|`NavigationStack(path:)`| Decouples links from destinations; type-safe, value-driven routing. |
462
+
|`NavigationLink(destination:)`|`NavigationLink(value:)` + `.navigationDestination(for:)`| Prevents destination views from being eagerly instantiated before tap. |
463
+
|`foregroundColor(_:)`|`foregroundStyle(_:)`| Supports rich styles, gradients, semantic shapes, and system materials. |
464
+
|`accentColor(_:)`|`tint(_:)`| Localized view-scoped styling without global pollution. |
465
+
|`cornerRadius(_:)`|`clipShape(.rect(cornerRadius:))`| Standardized, shape-aware clipping with selective corner support. |
466
+
|`@StateObject`|`@State` with `@Observable`| No wrapper overhead; fields tracked automatically. |
0 commit comments