mirror of
https://github.com/Dimillian/Skills.git
synced 2026-04-27 14:57:40 +00:00
Enhance swiftui-patterns skill
This commit is contained in:
parent
1ec2d7af93
commit
098389dd35
6 changed files with 238 additions and 54 deletions
|
|
@ -30,13 +30,15 @@ Choose a track based on your goal:
|
|||
- Use modern SwiftUI state (`@State`, `@Binding`, `@Observable`, `@Environment`) and avoid unnecessary view models.
|
||||
- If the deployment target includes iOS 16 or earlier and cannot use the Observation API introduced in iOS 17, fall back to `ObservableObject` with `@StateObject` for root ownership, `@ObservedObject` for injected observation, and `@EnvironmentObject` only for truly shared app-level state.
|
||||
- Prefer composition; keep views small and focused.
|
||||
- Use async/await with `.task` and explicit loading/error states.
|
||||
- Use async/await with `.task` and explicit loading/error states. For restart, cancellation, and debouncing guidance, read `references/async-state.md`.
|
||||
- Keep shared app services in `@Environment`, but prefer explicit initializer injection for feature-local dependencies and models. For root wiring patterns, read `references/app-wiring.md`.
|
||||
- Prefer the newest SwiftUI API that fits the deployment target and call out the minimum OS whenever a pattern depends on it.
|
||||
- Maintain existing legacy patterns only when editing legacy files.
|
||||
- Follow the project's formatter and style guide.
|
||||
- **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
|
||||
## State ownership summary
|
||||
|
||||
Use the narrowest state tool that matches the ownership model:
|
||||
|
||||
|
|
@ -45,51 +47,21 @@ Use the narrowest state tool that matches the ownership model:
|
|||
| 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 |
|
||||
| Child reads or mutates an injected 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
|
||||
## Cross-cutting references
|
||||
|
||||
- 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.
|
||||
- `references/navigationstack.md`: navigation ownership, per-tab history, and enum routing.
|
||||
- `references/sheets.md`: centralized modal presentation and enum-driven sheets.
|
||||
- `references/deeplinks.md`: URL handling and routing external links into app destinations.
|
||||
- `references/app-wiring.md`: root dependency graph, environment usage, and app shell wiring.
|
||||
- `references/async-state.md`: `.task`, `.task(id:)`, cancellation, debouncing, and async UI state.
|
||||
- `references/previews.md`: `#Preview`, fixtures, mock environments, and isolated preview setup.
|
||||
- `references/performance.md`: stable identity, observation scope, lazy containers, and render-cost guardrails.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
|
|
@ -99,21 +71,14 @@ Choose the ownership location first, then pick the wrapper. Do not introduce a r
|
|||
- 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, 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.
|
||||
3. Sketch the view hierarchy, routing model, and presentation points; extract repeated parts into subviews. For complex navigation, read `references/navigationstack.md`, `references/sheets.md`, or `references/deeplinks.md`. **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. Read `references/async-state.md` when the work depends on changing inputs or cancellation.
|
||||
5. Add previews for the primary and secondary states, then add accessibility labels or identifiers when the UI is interactive. Read `references/previews.md` when the view needs fixtures or injected mock dependencies.
|
||||
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. Read `references/performance.md` if the screen is large, scroll-heavy, or frequently updated. 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
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,13 @@ Show how to wire the app shell (TabView + NavigationStack + sheets) and install
|
|||
2) A dedicated view modifier installs global dependencies and lifecycle tasks (auth state, streaming watchers, push tokens, data containers).
|
||||
3) Feature views pull only what they need from the environment; feature-specific state stays local.
|
||||
|
||||
## Dependency selection
|
||||
|
||||
- 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.
|
||||
|
||||
## Root shell example (generic)
|
||||
|
||||
```swift
|
||||
|
|
|
|||
96
swiftui-ui-patterns/references/async-state.md
Normal file
96
swiftui-ui-patterns/references/async-state.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# Async state and task lifecycle
|
||||
|
||||
## Intent
|
||||
|
||||
Use this pattern when a view loads data, reacts to changing input, or coordinates async work that should follow the SwiftUI view lifecycle.
|
||||
|
||||
## Core rules
|
||||
|
||||
- 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.
|
||||
|
||||
## Example: load on appear
|
||||
|
||||
```swift
|
||||
struct DetailView: View {
|
||||
let id: String
|
||||
@State private var state: LoadState<Item> = .idle
|
||||
@Environment(ItemClient.self) private var client
|
||||
|
||||
var body: some View {
|
||||
content
|
||||
.task {
|
||||
await load()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var content: some View {
|
||||
switch state {
|
||||
case .idle, .loading:
|
||||
ProgressView()
|
||||
case .loaded(let item):
|
||||
ItemContent(item: item)
|
||||
case .failed(let error):
|
||||
ErrorView(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
private func load() async {
|
||||
state = .loading
|
||||
do {
|
||||
state = .loaded(try await client.fetch(id: id))
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
state = .failed(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example: restart on input change
|
||||
|
||||
```swift
|
||||
struct SearchView: View {
|
||||
@State private var query = ""
|
||||
@State private var results: [ResultItem] = []
|
||||
@Environment(SearchClient.self) private var client
|
||||
|
||||
var body: some View {
|
||||
List(results) { item in
|
||||
Text(item.title)
|
||||
}
|
||||
.searchable(text: $query)
|
||||
.task(id: query) {
|
||||
try? await Task.sleep(for: .milliseconds(250))
|
||||
guard !Task.isCancelled, !query.isEmpty else {
|
||||
results = []
|
||||
return
|
||||
}
|
||||
do {
|
||||
results = try await client.search(query)
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
results = []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## When to move work out of the view
|
||||
|
||||
- If the async flow spans multiple screens or must survive view dismissal, move it into a service or model.
|
||||
- If the view is mostly coordinating app-level lifecycle or account changes, wire it at the app shell in `app-wiring.md`.
|
||||
- If retry, caching, or offline policy becomes complex, keep the policy in the client/service and leave the view with simple state transitions.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Do not start network work directly from `body`.
|
||||
- Do not ignore cancellation for searches, typeahead, or rapidly changing selections.
|
||||
- Avoid storing derived async state in multiple places when one source of truth is enough.
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
# Components Index
|
||||
|
||||
Use this file to find component-specific guidance. Each entry lists when to use it.
|
||||
Use this file to find component and cross-cutting guidance. Each entry lists when to use it.
|
||||
|
||||
## Available components
|
||||
|
||||
- TabView: `references/tabview.md` — Use when building a tab-based app or any tabbed feature set.
|
||||
- NavigationStack: `references/navigationstack.md` — Use when you need push navigation and programmatic routing, especially per-tab history.
|
||||
- Sheets and modal routing: `references/sheets.md` — Use when you want centralized, enum-driven sheet presentation.
|
||||
- App wiring and dependency graph: `references/app-wiring.md` — Use to wire TabView + NavigationStack + sheets at the root and install global dependencies.
|
||||
- Form and Settings: `references/form.md` — Use for settings, grouped inputs, and structured data entry.
|
||||
- macOS Settings: `references/macos-settings.md` — Use when building a macOS Settings window with SwiftUI's Settings scene.
|
||||
- Split views and columns: `references/split-views.md` — Use for iPad/macOS multi-column layouts or custom secondary columns.
|
||||
|
|
@ -31,6 +30,13 @@ Use this file to find component-specific guidance. Each entry lists when to use
|
|||
- Loading & placeholders: `references/loading-placeholders.md` — Use for redacted skeletons, empty states, and loading UX.
|
||||
- Lightweight clients: `references/lightweight-clients.md` — Use for small, closure-based API clients injected into stores.
|
||||
|
||||
## Cross-cutting references
|
||||
|
||||
- App wiring and dependency graph: `references/app-wiring.md` — Use to wire the app shell, install shared dependencies, and decide what belongs in the environment.
|
||||
- Async state and task lifecycle: `references/async-state.md` — Use when a view loads data, reacts to changing input, or needs cancellation/debouncing guidance.
|
||||
- Previews: `references/previews.md` — Use when adding `#Preview`, fixtures, mock environments, or isolated preview setup.
|
||||
- Performance guardrails: `references/performance.md` — Use when a screen is large, scroll-heavy, frequently updated, or showing signs of avoidable re-renders.
|
||||
|
||||
## Planned components (create files as needed)
|
||||
|
||||
- Web content: create `references/webview.md` — Use for embedded web content or in-app browsing.
|
||||
|
|
|
|||
62
swiftui-ui-patterns/references/performance.md
Normal file
62
swiftui-ui-patterns/references/performance.md
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# Performance guardrails
|
||||
|
||||
## Intent
|
||||
|
||||
Use these rules when a SwiftUI screen is large, scroll-heavy, frequently updated, or at risk of unnecessary recomputation.
|
||||
|
||||
## Core rules
|
||||
|
||||
- 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.
|
||||
|
||||
## Example: stable identity
|
||||
|
||||
```swift
|
||||
ForEach(items) { item in
|
||||
Row(item: item)
|
||||
}
|
||||
```
|
||||
|
||||
Prefer that over index-based identity when the collection can change order:
|
||||
|
||||
```swift
|
||||
ForEach(Array(items.enumerated()), id: \.offset) { _, item in
|
||||
Row(item: item)
|
||||
}
|
||||
```
|
||||
|
||||
## Example: move expensive work out of body
|
||||
|
||||
```swift
|
||||
struct FeedView: View {
|
||||
let items: [FeedItem]
|
||||
|
||||
private var sortedItems: [FeedItem] {
|
||||
items.sorted(using: KeyPathComparator(\.createdAt, order: .reverse))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List(sortedItems) { item in
|
||||
FeedRow(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If the work is more expensive than a small derived property, move it into a model, store, or helper that updates less often.
|
||||
|
||||
## When to investigate further
|
||||
|
||||
- Janky scrolling in long feeds or grids
|
||||
- Typing lag from search or form validation
|
||||
- Overly broad view updates when one small piece of state changes
|
||||
- Large screens with many conditionals or repeated formatting work
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Recomputing heavy transforms every render
|
||||
- Observing a large object from many descendants when only one field matters
|
||||
- Building custom scroll containers when `List`, `LazyVStack`, or `LazyHGrid` would already solve the problem
|
||||
48
swiftui-ui-patterns/references/previews.md
Normal file
48
swiftui-ui-patterns/references/previews.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Previews
|
||||
|
||||
## Intent
|
||||
|
||||
Use previews to validate layout, state wiring, and injected dependencies without relying on a running app or live services.
|
||||
|
||||
## Core rules
|
||||
|
||||
- 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.
|
||||
|
||||
## Example: simple preview states
|
||||
|
||||
```swift
|
||||
#Preview("Loaded") {
|
||||
ProfileView(profile: .fixture)
|
||||
}
|
||||
|
||||
#Preview("Empty") {
|
||||
ProfileView(profile: nil)
|
||||
}
|
||||
```
|
||||
|
||||
## Example: preview with injected dependencies
|
||||
|
||||
```swift
|
||||
#Preview("Search results") {
|
||||
SearchView()
|
||||
.environment(SearchClient.preview(results: [.fixture, .fixture2]))
|
||||
.environment(Theme.preview)
|
||||
}
|
||||
```
|
||||
|
||||
## Preview checklist
|
||||
|
||||
- Does the preview install every required environment dependency?
|
||||
- Does it cover at least one success path and one non-happy path?
|
||||
- Are fixtures stable and small enough to be read quickly?
|
||||
- Can the preview render without network, auth, or app-global initialization?
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Do not hide preview crashes by making dependencies optional if the production view requires them.
|
||||
- Avoid huge inline fixtures when a named sample is easier to read.
|
||||
- Do not couple previews to global shared singletons unless the project has no alternative.
|
||||
Loading…
Reference in a new issue