Skip to content

Commit 6cdf7eb

Browse files
UnbreakableMJAntigravity (Gemini 3.5 Flash)
andcommitted
feat(skills): improve spacecraft-swift-guidelines with SwiftUI patterns
Integrate SwiftUI best practices inspired by efremidze/swift-patterns-skill: - Add property wrapper selection (State, Binding, Observable, Environment). - Add view composition patterns (unidirectional flow: data down, events up). - Add stable list identity rules (identifiable IDs, avoiding .indices). - Add lifecycle-scoped async tasking (.task vs .task(id:)). - Add modern SwiftUI API migration mappings. Co-Authored-By: Antigravity (Gemini 3.5 Flash) <noreply@google.com>
1 parent 1442e22 commit 6cdf7eb

4 files changed

Lines changed: 202 additions & 1 deletion

File tree

spacecraft-swift-guidelines.skill

3.15 KB
Binary file not shown.

spacecraft-swift-guidelines.zip

3.15 KB
Binary file not shown.

spacecraft-swift-guidelines/SKILL.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ website: https://Construct.SpacecraftSoftware.org/
1919
- **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.
2020
- **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.
2121
- **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.
2223

2324
## Memory Safety & Code Integrity
2425
- **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:
5051
3. **Structured Cancel Check:** Ensure long-running loops check for task cancellation using `Task.checkCancellation()` or checking `Task.isCancelled`.
5152
4. **Zipped Arguments:** In parameter testing, use `zip()` to align test arguments instead of Cartesian products.
5253
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.
5356

5457
## Build, Tooling & CI (Non-Negotiable)
5558
- **Toolchain floor:** Swift 6.2, Xcode 26+.
@@ -64,6 +67,9 @@ Always choose the concurrency model corresponding to the workload:
6467
- Updating UI-bound properties from background threads (always isolate ViewModels to `@MainActor`).
6568
- Spawning detached unstructured tasks in hot loops.
6669
- 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.
6773

6874
## Pre-Commit Checklist (Verify Every Time)
6975
- [ ] Concurrency checking set to Strict Mode; warnings treated as errors
@@ -75,9 +81,13 @@ Always choose the concurrency model corresponding to the workload:
7581
- [ ] Test suites use the new Swift Testing `@Suite` and `@Test` macros
7682
- [ ] Parameterized tests use `arguments` and `zip` to keep inputs aligned
7783
- [ ] 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
7888

7989
## 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.
8191
- *Further reading* (consulted for background only): Swift Concurrency Proposals (SE-0461, SE-0470), SwiftUI Performance Audits, Swift Testing Guide, and Apple Security Guidelines.
8292

8393
When the user requests Swift code or review, activate this skill, apply the checklist, and produce code a senior Spacecraft systems engineer would ship.

spacecraft-swift-guidelines/references/Spacecraft_Swift_Guidelines.md

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,194 @@ Before merging Swift code, verify:
279279
3. Every asynchronous closure capturing `self` uses the `[weak self]` capture list.
280280
4. ViewModels update state variables on the `@MainActor`.
281281
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
297+
```swift
298+
import SwiftUI
299+
300+
// 1. Immutable Data Model
301+
public struct TaskItem: Identifiable, Sendable, Hashable {
302+
public let id: UUID
303+
public let title: String
304+
public let isCompleted: Bool
305+
}
306+
307+
// 2. Main ViewModel (MainActor isolated state owner)
308+
@MainActor
309+
@Observable
310+
public final class TaskListViewModel {
311+
public var tasks: [TaskItem] = []
312+
313+
public init() {}
314+
315+
public func toggleTask(id: UUID) {
316+
if let index = tasks.firstIndex(where: { $0.id == id }) {
317+
let task = tasks[index]
318+
tasks[index] = TaskItem(id: task.id, title: task.title, isCompleted: !task.isCompleted)
319+
}
320+
}
321+
}
322+
323+
// 3. Parent View (Owns layout and coordinates events)
324+
public struct TaskListView: View {
325+
@State private var viewModel = TaskListViewModel()
326+
@State private var isFilterActive = false
327+
328+
public var body: some View {
329+
NavigationStack {
330+
List(viewModel.tasks) { task in
331+
// Pass immutable value for display, and callback closure for actions
332+
TaskRowView(task: task) {
333+
viewModel.toggleTask(id: task.id)
334+
}
335+
}
336+
.navigationTitle("Tasks")
337+
.toolbar {
338+
ToolbarItem(placement: .primaryAction) {
339+
Toggle("Filter", isOn: $isFilterActive) // Pass binding for edits
340+
}
341+
}
342+
}
343+
}
344+
}
345+
346+
// 4. Child View (Reusable, display-only + action callback)
347+
struct TaskRowView: View {
348+
let task: TaskItem // Display-only immutable
349+
let onToggle: () -> Void // Event callback
350+
351+
var body: some View {
352+
HStack {
353+
Text(task.title)
354+
.strikethrough(task.isCompleted)
355+
Spacer()
356+
Button(action: onToggle) {
357+
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
358+
.foregroundStyle(task.isCompleted ? .green : .secondary)
359+
}
360+
.buttonStyle(.plain)
361+
}
362+
}
363+
}
364+
```
365+
366+
---
367+
368+
## 9. SwiftUI List Performance & Identity
369+
370+
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
378+
```swift
379+
import SwiftUI
380+
381+
public struct TelemetryNode: Identifiable, Sendable {
382+
public let id: String // Unique and stable identifier (e.g. device serial number)
383+
public let name: String
384+
public let signalStrength: Double
385+
}
386+
387+
struct NodeMonitorView: View {
388+
let nodes: [TelemetryNode]
389+
390+
var body: some View {
391+
// Correct: List bound to Identifiable elements
392+
List(nodes) { node in
393+
HStack {
394+
Text(node.name)
395+
Spacer()
396+
Text("\(Int(node.signalStrength * 100))%")
397+
.foregroundStyle(node.signalStrength < 0.2 ? .red : .primary)
398+
}
399+
}
400+
}
401+
}
402+
```
403+
404+
---
405+
406+
## 10. SwiftUI Lifecycle-Scoped Concurrency
407+
408+
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+
import SwiftUI
418+
419+
struct LogMonitorView: View {
420+
let sourceURL: URL
421+
@State private var logs: [String] = []
422+
@State private var 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+
await fetchLogs()
431+
}
432+
// Automatically cancels and restarts if sourceURL changes
433+
.task(id: sourceURL) {
434+
await fetchLogs()
435+
}
436+
}
437+
438+
private func fetchLogs() async {
439+
do {
440+
let (data, _) = try await URLSession.shared.data(from: sourceURL)
441+
if let decodedLogs = try? JSONDecoder().decode([String].self, from: data) {
442+
self.logs = decodedLogs
443+
}
444+
} catch {
445+
// Handle error or cancel
446+
}
447+
}
448+
}
449+
```
450+
451+
---
452+
453+
## 11. Modern SwiftUI API Migration Checklist
454+
455+
Prefer modern SwiftUI APIs to optimize performance and take advantage of advanced compiler checks.
456+
457+
### Legacy → Modern API Mapping
458+
459+
| Legacy Pattern | Modern Replacement (iOS 17+) | Notes / Rationale |
460+
| :--- | :--- | :--- |
461+
| `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. |
467+
| `@EnvironmentObject` | `@Environment(MyType.self)` | Strictly type-safe environment lookup. |
468+
| `onChange(of: value) { val in }` | `onChange(of: value) { old, new in }` | Access both preceding and current states (iOS 17+). |
469+
| `onAppear { Task { await work() } }` | `.task { await work() }` | Automates task cancellation when the view hierarchy is dismissed. |
470+
| `UIScreen.main.bounds` | `containerRelativeFrame()` | Avoids hardcoded screen sizes; adapts properly to multi-window split views. |
471+
| `.sheet(isPresented:)` | `.sheet(item:)` | Cleaner state-driven presentation flow. |
472+

0 commit comments

Comments
 (0)