Update SKILL.md to refine project scaffolding instructions and introduce a state ownership matrix. Added detailed navigation and routing guidelines, preview guidance, async task lifecycle recommendations, performance guardrails, environment injection policies, and anti-patterns to enhance SwiftUI development practices.

This commit is contained in:
Thomas Ricouard 2026-03-15 10:44:45 +01:00
parent ce79ef24da
commit 1ec2d7af93

View file

@ -20,7 +20,7 @@ Choose a track based on your goal:
### New project scaffolding
- Start with `references/app-scaffolding-wiring.md` to wire TabView + NavigationStack + sheets.
- Start with `references/app-wiring.md` to wire TabView + NavigationStack + sheets.
- Add a minimal `AppTab` and `RouterPath` based on the provided skeletons.
- Choose the next component reference based on the UI you need first (TabView, NavigationStack, Sheets).
- Expand the route and sheet enums as new screens are added.
@ -36,14 +36,84 @@ Choose a track based on your goal:
- **Sheets**: Prefer `.sheet(item:)` over `.sheet(isPresented:)` when state represents a selected model. Avoid `if let` inside a sheet body. Sheets should own their actions and call `dismiss()` internally instead of forwarding `onCancel`/`onConfirm` closures.
- **Scroll-driven reveals**: Prefer deriving a normalized progress value from scroll offset and driving the visual state from that single source of truth. Avoid parallel gesture state machines unless scroll alone cannot express the interaction.
## State ownership matrix
Use the narrowest state tool that matches the ownership model:
| Scenario | Preferred pattern |
| --- | --- |
| Local UI state owned by one view | `@State` |
| Child mutates parent-owned value state | `@Binding` |
| Root-owned reference model on iOS 17+ | `@State` with an `@Observable` type |
| Child reads or mutates an injected `@Observable` model | Pass it explicitly as a stored property |
| Shared app service or configuration | `@Environment(Type.self)` |
| Legacy reference model on iOS 16 and earlier | `@StateObject` at the root, `@ObservedObject` when injected |
Choose the ownership location first, then pick the wrapper. Do not introduce a reference model when plain value state is enough.
## Navigation and routing
- Use `NavigationStack` and local route state for most features. Keep navigation ownership close to the feature unless multiple entry points truly need shared routing.
- In tabbed apps, prefer one navigation history per tab instead of a single shared stack for the entire app.
- Use enum-driven sheet, alert, and destination routing when presentation is mutually exclusive or deep-linkable.
- Centralize route enums only when the feature has deep links, handoff from multiple surfaces, or cross-feature navigation requirements.
- Avoid global routers for simple push flows that can stay local to one screen tree.
- See `references/navigationstack.md`, `references/sheets.md`, and `references/deeplinks.md` when the view needs more than straightforward local navigation.
## Preview guidance
- Add `#Preview` coverage for the primary state plus important secondary states such as loading, empty, and error.
- Use deterministic fixtures, mocks, and sample data. Do not make previews depend on live network calls, real databases, or global singletons.
- Install required environment dependencies directly in the preview so the view can render in isolation.
- Keep preview setup close to the view until it becomes noisy; then extract lightweight preview helpers or fixtures.
- If a preview crashes, fix the state initialization or dependency wiring before expanding the feature further.
## Async and task lifecycle
- Use `.task` for load-on-appear work that belongs to the view lifecycle.
- Use `.task(id:)` when async work should restart for a changing input such as a query, selection, or identifier.
- Treat cancellation as a normal path for view-driven tasks. Check `Task.isCancelled` in longer flows and avoid surfacing cancellation as a user-facing error.
- Debounce or coalesce user-driven async work such as search before it fans out into repeated requests.
- Keep UI-facing models and mutations main-actor-safe; do background work in services, then publish the result back to UI state.
## Performance guardrails
- Give `ForEach` and list content stable identity. Do not use unstable indices as identity when the collection can reorder or mutate.
- Keep expensive filtering, sorting, and formatting out of `body`; precompute or move it into a model/helper when it is not trivial.
- Narrow observation scope so only the views that read changing state need to update.
- Prefer lazy containers for larger scrolling content and extract subviews when only part of a screen changes frequently.
- Avoid swapping entire top-level view trees for small state changes; keep a stable root view and vary localized sections or modifiers.
## Environment injection policy
- Use `@Environment` for app-level services, shared clients, theme/configuration, and values that many descendants genuinely need.
- Prefer initializer injection for feature-local dependencies and models. Do not move a dependency into the environment just to avoid passing one or two arguments.
- Keep mutable feature state out of the environment unless it is intentionally shared across broad parts of the app.
- Use `@EnvironmentObject` only as a legacy fallback or when the project already standardizes on it for a truly shared object.
## Anti-patterns
- Giant views that mix layout, business logic, networking, routing, and formatting in one file.
- Multiple boolean flags for mutually exclusive sheets, alerts, or navigation destinations.
- Live service calls directly inside `body`-driven code paths instead of view lifecycle hooks or injected models/services.
- Reaching for `AnyView` to work around type mismatches that should be solved with better composition.
- Defaulting every shared dependency to `@EnvironmentObject` or a global router without a clear ownership reason.
## Platform and version guidance
- Prefer the newest SwiftUI API that fits the deployment target, but call out the minimum OS whenever guidance depends on it.
- When using iOS 17+ Observation or iOS 26+ UI APIs, include a fallback for older targets if the skill is meant to support them.
- Keep compatibility notes next to the rule or example they affect so the fallback is easy to apply.
- Avoid mixing the new Observation system and legacy Combine-based observation in the same feature unless compatibility requires it.
## Workflow for a new SwiftUI view
1. Define the view's state and its ownership location.
2. Identify dependencies to inject via `@Environment`.
3. Sketch the view hierarchy and extract repeated parts into subviews. **Build and verify no compiler errors before proceeding.**
4. Implement async loading with `.task` and explicit state enum if needed.
5. Add accessibility labels or identifiers when the UI is interactive.
6. Validate with a build: confirm no compiler errors, check that previews render without crashing, and ensure state changes propagate correctly. For common SwiftUI compilation errors — missing `@State` annotations, ambiguous `ViewBuilder` closures, or mismatched generic types — resolve them before updating callsites. **If the build fails:** read the error message carefully, fix the identified issue, then rebuild before proceeding to the next step. If a preview crashes, isolate the offending subview, confirm its state initialisation is valid, and re-run the preview before continuing.
1. Define the view's state, ownership location, and minimum OS assumptions before writing UI code.
2. Identify which dependencies belong in `@Environment` and which should stay as explicit initializer inputs.
3. Sketch the view hierarchy, routing model, and presentation points; extract repeated parts into subviews. **Build and verify no compiler errors before proceeding.**
4. Implement async loading with `.task` or `.task(id:)`, plus explicit loading and error states when needed.
5. Add previews for the primary and secondary states, then add accessibility labels or identifiers when the UI is interactive.
6. Validate with a build: confirm no compiler errors, check that previews render without crashing, ensure state changes propagate correctly, and sanity-check that list identity and observation scope will not cause avoidable re-renders. For common SwiftUI compilation errors — missing `@State` annotations, ambiguous `ViewBuilder` closures, or mismatched generic types — resolve them before updating callsites. **If the build fails:** read the error message carefully, fix the identified issue, then rebuild before proceeding to the next step. If a preview crashes, isolate the offending subview, confirm its state initialisation is valid, and re-run the preview before continuing.
## Component references