ios-swift-skill 1.0.0
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.
- package/SKILL.md +59 -0
- package/agents/openai.yaml +4 -0
- package/bin/install.js +43 -0
- package/package.json +31 -0
- package/references/mobile-design-system.md +235 -0
- package/references/swiftui-clean-code.md +311 -0
package/SKILL.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ios-swift-skill
|
|
3
|
+
description: iOS Swift and SwiftUI product-engineering guidance for HIG-aligned mobile design systems, view specs, and maintainable code. Use when designing or implementing iPhone/iPad screens, creating Apple HIG component inventories, applying semantic color, Dynamic Type, Liquid Glass/materials, motion, accessibility, SwiftUI Observation, structured concurrency, dependency injection, testing, or reviewing Swift code quality.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# iOS Swift Skill
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Use this skill to design and implement iOS app surfaces that feel native, accessible, and maintainable. Start with Apple Human Interface Guidelines component names and native SwiftUI/UIKit behavior, then add app-specific composites only when they clarify product behavior.
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
1. Identify the task mode:
|
|
15
|
+
- For mobile UI design, view specs, HIG inventories, design-system tokens, layout, motion, typography, accessibility, or Liquid Glass/material decisions, read `references/mobile-design-system.md`.
|
|
16
|
+
- For Swift, SwiftUI, architecture, state, concurrency, dependencies, tests, or code-review work, read `references/swiftui-clean-code.md`.
|
|
17
|
+
- For a feature that includes both UI and implementation, read both references before editing or generating code.
|
|
18
|
+
2. Prefer native platform primitives first: `NavigationStack`, `List`, `Button`, `Label`, `LabeledContent`, `ToolbarItem`, `TabView`, `Sheet`, `Menu`, `Picker`, `Toggle`, `ProgressView`, semantic colors, Dynamic Type, and system materials.
|
|
19
|
+
3. Keep app-specific naming at the composite layer. Use HIG names for components and descriptive product-agnostic names for combinations, such as "profile card", "activity row", "onboarding prompt", or "upgrade sheet".
|
|
20
|
+
4. Treat loading, empty, error, modal, and permission states as part of the closest primary view unless the product flow needs a distinct route.
|
|
21
|
+
5. Verify accessibility and maintainability before finishing: Dynamic Type, VoiceOver labels, Reduce Motion, Increase Contrast, dark mode, actor isolation, dependency injection, deterministic tests, and explicit state modeling.
|
|
22
|
+
|
|
23
|
+
## View Spec Shape
|
|
24
|
+
|
|
25
|
+
When creating or revising an iOS view spec, include these sections when relevant:
|
|
26
|
+
|
|
27
|
+
- `Labels and Copy`
|
|
28
|
+
- `HIG Component Inventory`
|
|
29
|
+
- `Layout and Position`
|
|
30
|
+
- `Color and Typography`
|
|
31
|
+
- `Functionality`
|
|
32
|
+
- `States`
|
|
33
|
+
- `Data Dependencies`
|
|
34
|
+
|
|
35
|
+
In `HIG Component Inventory`, name Apple HIG components first, then explain how the app composes them. Avoid inventing custom component names for native controls.
|
|
36
|
+
|
|
37
|
+
## Implementation Defaults
|
|
38
|
+
|
|
39
|
+
- Keep SwiftUI views declarative. Put side effects in explicit actions, `.task(id:)`, observable models, clients, repositories, or actors.
|
|
40
|
+
- Use `@Observable` models with `@State` for new SwiftUI screen state unless existing code requires `ObservableObject`.
|
|
41
|
+
- Model mutually exclusive screen states with an enum instead of boolean combinations.
|
|
42
|
+
- Inject networking, persistence, clocks, UUID generation, feature flags, analytics, and notification scheduling.
|
|
43
|
+
- Make async work cancellable and actor isolation explicit. Use `@MainActor` for UI models unless isolation is deliberately elsewhere.
|
|
44
|
+
- Use the design system for semantic color, typography, spacing, motion, materials, and HIG component naming. Do not hard-code one-off fonts, colors, or corner radii when a token or native component applies.
|
|
45
|
+
|
|
46
|
+
## Review Checklist
|
|
47
|
+
|
|
48
|
+
- Does the UI use native HIG components and semantic system behavior before custom composition?
|
|
49
|
+
- Does the view body read as layout rather than business logic?
|
|
50
|
+
- Are state transitions explicit, testable, and accessible?
|
|
51
|
+
- Are colors, fonts, spacing, materials, and motion aligned with the design system reference?
|
|
52
|
+
- Are Dynamic Type, VoiceOver, Reduce Motion, Increase Contrast, dark mode, loading, empty, and error states covered?
|
|
53
|
+
- Are async tasks cancellable, dependencies injected, and cross-task values safely `Sendable` or actor-isolated?
|
|
54
|
+
- Are tests deterministic and focused on user-visible or domain behavior?
|
|
55
|
+
|
|
56
|
+
## References
|
|
57
|
+
|
|
58
|
+
- `references/mobile-design-system.md`: iOS design-system tokens, HIG component taxonomy, motion map, layout zones, and view-spec guidance.
|
|
59
|
+
- `references/swiftui-clean-code.md`: SwiftUI architecture, Observation state, reusable UI, concurrency, dependencies, naming, accessibility, localization, testing, and review guidance.
|
package/bin/install.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const os = require("node:os");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
|
|
7
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
8
|
+
const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
9
|
+
const destination = path.join(codexHome, "skills", "ios-swift-skill");
|
|
10
|
+
const force = process.argv.includes("--force");
|
|
11
|
+
const help = process.argv.includes("--help") || process.argv.includes("-h");
|
|
12
|
+
|
|
13
|
+
if (help) {
|
|
14
|
+
console.log(`Install the iOS Swift skill for Codex.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
npx ios-swift-skill [--force]
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--force Replace an existing installation
|
|
21
|
+
-h, --help Show this help`);
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (fs.existsSync(destination)) {
|
|
26
|
+
if (!force) {
|
|
27
|
+
console.error(`Already installed at ${destination}`);
|
|
28
|
+
console.error("Run again with --force to replace it.");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
35
|
+
|
|
36
|
+
for (const entry of ["SKILL.md", "agents", "references"]) {
|
|
37
|
+
fs.cpSync(path.join(packageRoot, entry), path.join(destination, entry), {
|
|
38
|
+
recursive: true,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(`Installed ios-swift-skill to ${destination}`);
|
|
43
|
+
console.log("Restart Codex to load the skill.");
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ios-swift-skill",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Install an iOS Swift and SwiftUI engineering skill for Codex",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/DongkuKim/ios-swift-skill.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/DongkuKim/ios-swift-skill#readme",
|
|
11
|
+
"bugs": "https://github.com/DongkuKim/ios-swift-skill/issues",
|
|
12
|
+
"bin": {
|
|
13
|
+
"ios-swift-skill": "bin/install.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"SKILL.md",
|
|
17
|
+
"agents",
|
|
18
|
+
"references",
|
|
19
|
+
"bin"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"codex",
|
|
26
|
+
"skill",
|
|
27
|
+
"ios",
|
|
28
|
+
"swift",
|
|
29
|
+
"swiftui"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# iOS Mobile Design System Reference
|
|
2
|
+
|
|
3
|
+
Use this reference for iOS-first mobile design systems, HIG component inventories, SwiftUI/UIKit view specs, and design review. Keep product-specific names out of the system layer; describe products through native components and app-level composites.
|
|
4
|
+
|
|
5
|
+
## Platform Baseline
|
|
6
|
+
|
|
7
|
+
| Token | Value |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| Device baseline | iPhone, 390 x 844 pt |
|
|
10
|
+
| Safe areas | Top 47 pt, bottom 34 pt on the baseline device |
|
|
11
|
+
| Grid | 8 pt spacing system |
|
|
12
|
+
| Horizontal margin | 20 pt standard, 16 pt for dense lists |
|
|
13
|
+
| Corner radius | 8 pt for cards and sheets unless a card-style object requires 16 pt |
|
|
14
|
+
| Touch target | 44 x 44 pt minimum |
|
|
15
|
+
| Top chrome | Use `Toolbars` for top actions |
|
|
16
|
+
| Bottom chrome | Use `Tab bars` for primary app navigation |
|
|
17
|
+
| Material system | Implement Liquid Glass through standard system components on supported Apple platform releases |
|
|
18
|
+
| Motion system | Prefer standard system component motion, with Reduce Motion fallbacks for every custom motion |
|
|
19
|
+
|
|
20
|
+
## Color Tokens
|
|
21
|
+
|
|
22
|
+
Apple HIG color guidance favors semantic, adaptive colors over hard-coded values. Use system colors for platform-native surfaces, foreground content, controls, and status. Brand and premium moments should come from hierarchy, material, motion, iconography, interaction priority, and carefully scoped accent usage rather than broad custom color systems.
|
|
23
|
+
|
|
24
|
+
| Token | Apple basis | Usage |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| `color.backgroundPrimary` | `systemBackground` | Primary app canvas outside grouped list views |
|
|
27
|
+
| `color.backgroundGrouped` | `systemGroupedBackground` | List-heavy and grouped backgrounds |
|
|
28
|
+
| `color.surfacePrimary` | `secondarySystemBackground` | Elevated boxes, field containers, and rows on primary backgrounds |
|
|
29
|
+
| `color.surfaceGrouped` | `secondarySystemGroupedBackground` | Elevated boxes, field containers, and rows on grouped backgrounds |
|
|
30
|
+
| `color.surfaceTertiary` | `tertiarySystemBackground` or `tertiarySystemGroupedBackground` | Nested elements inside secondary surfaces |
|
|
31
|
+
| `color.textPrimary` | `label` | Primary labels, titles, prompts, and body copy |
|
|
32
|
+
| `color.textSecondary` | `secondaryLabel` | Metadata, helper labels, subtitles, and timestamps |
|
|
33
|
+
| `color.textTertiary` | `tertiaryLabel` | De-emphasized counters, quiet placeholders, and supplemental footnotes |
|
|
34
|
+
| `color.placeholder` | `placeholderText` | Empty text-field and text-view placeholder content |
|
|
35
|
+
| `color.separator` | `separator` | Hairlines, list dividers, and subtle card borders |
|
|
36
|
+
| `color.opaqueSeparator` | `opaqueSeparator` | Borders that must not show underlying content |
|
|
37
|
+
| `color.link` | `link` | Text links, secondary actions, and tappable inline copy |
|
|
38
|
+
| `color.tint` | System tint or app accent | Filled primary buttons, selected controls, progress fills, badges, and active gauges |
|
|
39
|
+
| `color.textOnTint` | Prominent control foreground | Text and symbols on filled tinted controls |
|
|
40
|
+
| `color.success` | `systemGreen` | Completed and unlocked states |
|
|
41
|
+
| `color.warning` | `systemOrange` | Urgent but recoverable notices |
|
|
42
|
+
| `color.danger` | `systemRed` | Destructive, failed, blocked, or irreversible states |
|
|
43
|
+
| `color.machineReadableInk` | Fixed high-contrast utility color | QR codes, barcodes, and other marks placed on light scannable surfaces |
|
|
44
|
+
| `color.machineReadableSurface` | Fixed high-contrast utility color | Background wells for machine-readable marks |
|
|
45
|
+
| `color.overlay` | Presentation dimming color | Dim backgrounds behind sheets and alerts when system presentation does not provide one |
|
|
46
|
+
|
|
47
|
+
### Color Usage Rules
|
|
48
|
+
|
|
49
|
+
- Do not hard-code system color hex values in implementation; use SwiftUI `Color` or UIKit `UIColor` semantic APIs.
|
|
50
|
+
- Do not redefine semantic color meanings: labels are for text, separators are for dividers, links are for interactive text, and backgrounds are for hierarchy.
|
|
51
|
+
- Do not rely on color alone for status, interactivity, limits, or selection. Pair color with labels, icons, glyph shape, copy, or layout.
|
|
52
|
+
- Test core views in light mode, dark mode, increased contrast, True Tone, and bright outdoor conditions.
|
|
53
|
+
- Reserve tinted control backgrounds for primary actions and status emphasis.
|
|
54
|
+
- Maintain machine-readable contrast for QR and barcode zones. Do not adapt machine-readable marks to dark-mode foreground colors.
|
|
55
|
+
|
|
56
|
+
## Liquid Glass and Materials
|
|
57
|
+
|
|
58
|
+
Use Liquid Glass as a system-provided material layer for controls and navigation, not as a decorative page background. Content remains the focus; materials create hierarchy for functional chrome such as bars, controls, sheets, popovers, and high-priority actions.
|
|
59
|
+
|
|
60
|
+
| Token | Apple basis | Usage |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| `material.systemGlass` | Standard SwiftUI/UIKit Liquid Glass adoption | Default for toolbars, tab bars, sheets, popovers, standard buttons, segmented controls, sliders, toggles, and native controls |
|
|
63
|
+
| `material.scrollEdgeGlass` | System scroll-edge effect | Navigation and action chrome above scrolling content |
|
|
64
|
+
| `material.contentSurface` | Semantic system background colors | Primary reading surfaces, profile content, rows, and long-form text |
|
|
65
|
+
| `material.customGlass` | SwiftUI `glassEffect`, UIKit `UIGlassEffect` | Rare custom functional controls that must float above content |
|
|
66
|
+
| `material.accessibleFallback` | Reduced transparency, reduced motion, and user-selected material appearance | Fallback to semantic surfaces, standard spacing, and static transitions |
|
|
67
|
+
|
|
68
|
+
### Material Rules
|
|
69
|
+
|
|
70
|
+
- Build with the latest SDKs and use standard SwiftUI/UIKit components first so Liquid Glass, control sizing, focus behavior, and platform animation updates are inherited automatically.
|
|
71
|
+
- Remove custom backgrounds from toolbars, tab bars, split views, sheets, popovers, and standard controls unless a product constraint requires them.
|
|
72
|
+
- Do not layer multiple glass controls on top of each other. Keep spacing generous enough that material elements do not crowd, overlap, or fight for focus.
|
|
73
|
+
- Use custom glass effects sparingly and only for functional elements. Do not apply glass to repeated cards, rows, or long reading areas by default.
|
|
74
|
+
- Let scroll containers use the system scroll-edge effect wherever chrome floats above content.
|
|
75
|
+
- Keep machine-readable marks on fixed high-contrast surfaces; never apply translucency to the mark itself.
|
|
76
|
+
|
|
77
|
+
## Motion Tokens
|
|
78
|
+
|
|
79
|
+
Apple HIG motion guidance favors purposeful, brief, realistic motion that provides feedback, preserves orientation, and supports continuity without distracting from content. Use standard system component motion first, then add custom motion only when it clarifies a state change, transition, or direct manipulation.
|
|
80
|
+
|
|
81
|
+
| Token | Apple basis | Applied to | Reduce Motion fallback |
|
|
82
|
+
| --- | --- | --- | --- |
|
|
83
|
+
| `motion.systemDefault` | Standard component motion | Native buttons, toolbars, tab bars, sheets, controls, menus, keyboard, and scroll behavior | Let the system reduce, replace, or remove motion |
|
|
84
|
+
| `motion.pressFeedback` | Brief feedback tied to touch | Primary and secondary buttons, list row taps, selected cards, and explicit actions | Instant highlight plus state label or symbol change |
|
|
85
|
+
| `motion.selectionChange` | Realistic state transition | Segmented controls, pickers, page controls, selected tokens, and filters | Instant selected state with color, label, or checkmark |
|
|
86
|
+
| `motion.presentation` | Context-preserving presentation | Sheets, popovers, alerts, action sheets, menus, context menus, and activity views | Crossfade or instant presentation from the same source context |
|
|
87
|
+
| `motion.navigation` | Orientation-preserving screen transition | Push/pop navigation, tab changes, detail opens, and verification flows | Crossfade while preserving title, back action, and focus order |
|
|
88
|
+
| `motion.scrollContinuity` | System scroll physics and scroll-edge effects | Scroll views, long lists, list chrome, sticky actions, and material bars over content | Standard scrolling with reduced effects and static chrome |
|
|
89
|
+
| `motion.disclosure` | Content expansion and collapse | Disclosure controls and expandable details | Instant expand/collapse while preserving layout order |
|
|
90
|
+
| `motion.listUpdate` | Insert, remove, reorder, and refresh feedback | Lists, tables, collections, unread rows, and activity rows | Non-animated row state change, optional brief fade |
|
|
91
|
+
| `motion.contentReveal` | Lightweight reveal for newly available content | Results, loaded images, generated content, preview cards, and unlocked states | Static content appears with clear status copy |
|
|
92
|
+
| `motion.progress` | Ongoing work or completion feedback | Progress indicators, gauges, readiness, import/export, and background work | Static progress value, percentage, or status label |
|
|
93
|
+
| `motion.statusUpdate` | Attention to important state changes | Notifications, Live Activities, unread badges, warnings, success states, and capacity changes | Static detail state with label and system symbol |
|
|
94
|
+
| `motion.none` | No independent motion | Machine-readable marks, core reading text, dense metadata, and passive rows | No fallback needed |
|
|
95
|
+
|
|
96
|
+
### Motion Rules
|
|
97
|
+
|
|
98
|
+
- Use standard SwiftUI/UIKit component motion wherever possible; system components adapt to input methods and accessibility settings.
|
|
99
|
+
- Add motion only when it communicates feedback, continuity, orientation, or state.
|
|
100
|
+
- Keep custom motion brief and precise. Frequent interactions like row taps, tab changes, or option selection should not make people wait.
|
|
101
|
+
- Make motion optional; never use motion as the only cue for success, failure, limits, availability, or unread state.
|
|
102
|
+
- Match motion to user intent and spatial origin. A view that opens from a row should return to that row.
|
|
103
|
+
- Let people interrupt or cancel motion. Do not block navigation, typing, scanning, or contact actions until a nonessential animation finishes.
|
|
104
|
+
- Avoid decorative parallax, looping motion, large background movement, or motion at screen edges.
|
|
105
|
+
- Use animated SF Symbols only for short, meaningful status changes such as success, warning, loading, or unlock confirmation.
|
|
106
|
+
- Keep QR codes, barcodes, and scannable zones static.
|
|
107
|
+
|
|
108
|
+
## Typography Tokens
|
|
109
|
+
|
|
110
|
+
Apple HIG typography guidance favors system fonts, built-in text styles, Dynamic Type, and clear hierarchy over fixed custom type scales. Treat the values below as iOS Large reference values, not fixed implementation sizes.
|
|
111
|
+
|
|
112
|
+
| Token | Apple text style | iOS Large size / leading | Default weight | Emphasized weight | Usage |
|
|
113
|
+
| --- | --- | --- | --- | --- | --- |
|
|
114
|
+
| `type.largeTitle` | Large Title | 34 / 41 pt | Regular | Bold | Welcome and major reveal titles |
|
|
115
|
+
| `type.title1` | Title 1 | 28 / 34 pt | Regular | Bold | Primary screen titles |
|
|
116
|
+
| `type.title2` | Title 2 | 22 / 28 pt | Regular | Bold | Section titles, sheet titles, and large card titles |
|
|
117
|
+
| `type.title3` | Title 3 | 20 / 25 pt | Regular | Semibold | Secondary screen headings and compact modal headings |
|
|
118
|
+
| `type.headline` | Headline | 17 / 22 pt | Semibold | Semibold | Row titles, button labels, and strong labels |
|
|
119
|
+
| `type.body` | Body | 17 / 22 pt | Regular | Semibold | Main copy, prompts, explanations, and multiline text |
|
|
120
|
+
| `type.callout` | Callout | 16 / 21 pt | Regular | Semibold | Secondary explanations and supporting copy |
|
|
121
|
+
| `type.subheadline` | Subhead | 15 / 20 pt | Regular | Semibold | Metadata, row subtitles, and compact secondary labels |
|
|
122
|
+
| `type.footnote` | Footnote | 13 / 18 pt | Regular | Semibold | Constraints, helper text, timestamps, and legal-adjacent notes |
|
|
123
|
+
| `type.caption` | Caption 1 | 12 / 16 pt | Regular | Semibold | Chips, badges, counters, and short status labels |
|
|
124
|
+
| `type.caption2` | Caption 2 | 11 / 13 pt | Regular | Semibold | Lowest-priority metadata only; avoid for essential content |
|
|
125
|
+
|
|
126
|
+
| Token | Value | Usage |
|
|
127
|
+
| --- | --- | --- |
|
|
128
|
+
| `text.defaultReadableSize.ios` | 17 pt | iOS default readable body size; use `type.body` for ordinary reading |
|
|
129
|
+
| `text.minimumSize.ios` | 11 pt | Absolute iOS minimum for nonessential labels; prefer larger text when space allows |
|
|
130
|
+
| `text.dynamicType.standardRange` | xSmall through xxxLarge | Required support range for normal Dynamic Type |
|
|
131
|
+
| `text.dynamicType.accessibilityRange` | AX1 through AX5 | Required support range for Larger Accessibility Text Sizes |
|
|
132
|
+
| `text.truncationPolicy` | Minimize truncation | Use multiline labels, wrapping, scrolling, or detail views before truncating useful text |
|
|
133
|
+
| `text.iconScaling` | Match text style | SF Symbols and meaningful glyphs scale with adjacent Dynamic Type text |
|
|
134
|
+
|
|
135
|
+
### Typography Rules
|
|
136
|
+
|
|
137
|
+
- Use built-in Dynamic Type text styles through SwiftUI `Font` or UIKit preferred font APIs; do not implement these values as fixed sizes.
|
|
138
|
+
- Use SF Pro through system APIs for interface text. Do not embed Apple system font files in the app.
|
|
139
|
+
- Prefer Regular, Medium, Semibold, and Bold. Avoid Ultralight, Thin, and Light weights, especially below `type.body`.
|
|
140
|
+
- Preserve visual hierarchy when Dynamic Type increases: primary content scales first, while lower-priority metadata can wrap, move, or collapse.
|
|
141
|
+
- Keep truncation rare. If useful text does not fit, prefer wrapping, scrolling, or a detail view over ellipsizing.
|
|
142
|
+
- Scale meaningful SF Symbols with adjacent text styles so icons remain readable at larger accessibility sizes.
|
|
143
|
+
|
|
144
|
+
## Apple HIG Component Taxonomy
|
|
145
|
+
|
|
146
|
+
Use these exact names in component inventory tables.
|
|
147
|
+
|
|
148
|
+
| Category | Component names |
|
|
149
|
+
| --- | --- |
|
|
150
|
+
| Content | Charts, Image views, Text views, Web views |
|
|
151
|
+
| Layout and organization | Boxes, Collections, Column views, Disclosure controls, Labels, Lists and tables, Lockups, Outline views, Split views, Tab views |
|
|
152
|
+
| Menus and actions | Activity views, Buttons, Context menus, Dock menus, Edit menus, Home Screen quick actions, Menus, Ornaments, Pop-up buttons, Pull-down buttons, The menu bar, Toolbars |
|
|
153
|
+
| Navigation and search | Path controls, Search fields, Sidebars, Tab bars, Token fields |
|
|
154
|
+
| Presentation | Action sheets, Alerts, Page controls, Panels, Popovers, Scroll views, Sheets, Windows |
|
|
155
|
+
| Selection and input | Color wells, Combo boxes, Digit entry views, Image wells, Pickers, Segmented controls, Sliders, Steppers, Text fields, Toggles, Virtual keyboards |
|
|
156
|
+
| Status | Gauges, Progress indicators, Rating indicators |
|
|
157
|
+
| System experiences | App Shortcuts, Complications, Controls, Live Activities, Notifications, Snippets, Status bars, Top Shelf, Watch faces, Widgets |
|
|
158
|
+
|
|
159
|
+
## Component Motion Map
|
|
160
|
+
|
|
161
|
+
Every view-level `HIG Component Inventory` inherits from this table. When a component appears in a view doc, apply the matching motion token unless the view state explicitly says otherwise.
|
|
162
|
+
|
|
163
|
+
| Apple HIG component | Motion applied | Guidance |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| Status bars | `motion.systemDefault` | Use system status behavior only; no custom motion |
|
|
166
|
+
| Toolbars | `motion.systemDefault`, `motion.scrollContinuity` | Let material and scroll-edge behavior adapt automatically above scrolling content |
|
|
167
|
+
| Tab bars | `motion.systemDefault`, `motion.selectionChange` | Use native tab selection motion; avoid custom tab icon bounces |
|
|
168
|
+
| Buttons | `motion.pressFeedback` | Brief press response for primary, secondary, contact, and card actions |
|
|
169
|
+
| Pull-down buttons, Pop-up buttons | `motion.pressFeedback`, `motion.presentation` | Button press opens anchored system menu or popover |
|
|
170
|
+
| Menus, Context menus | `motion.presentation` | Reveal from the invoking control or row; preserve source context |
|
|
171
|
+
| Action sheets, Activity views | `motion.presentation` | Present from the current task context; fallback to static system presentation |
|
|
172
|
+
| Sheets, Popovers | `motion.presentation` | Use native presentation and dismissal; avoid custom full-screen motion for scoped tasks |
|
|
173
|
+
| Alerts | `motion.presentation` | Use system alert presentation only for destructive, failure, or irreversible states |
|
|
174
|
+
| Scroll views | `motion.scrollContinuity` | Preserve native scroll physics and scroll-edge effects |
|
|
175
|
+
| Lists and tables | `motion.listUpdate`, `motion.pressFeedback` | Animate inserts/removals only when helpful; avoid decorative row staggering |
|
|
176
|
+
| Collections | `motion.listUpdate`, `motion.selectionChange` | Animate selected cards/options and catalog updates lightly |
|
|
177
|
+
| Disclosure controls | `motion.disclosure` | Expand/collapse content in place; keep source row visible |
|
|
178
|
+
| Tab views, Page controls | `motion.selectionChange`, `motion.navigation` | Use horizontal continuity for user-driven page movement |
|
|
179
|
+
| Search fields | `motion.systemDefault` | Use native focus, keyboard, and cancel-button behavior |
|
|
180
|
+
| Text fields, Token fields, Combo boxes | `motion.systemDefault`, `motion.selectionChange` | Use native keyboard/focus motion; token insert/delete gets brief selection motion |
|
|
181
|
+
| Virtual keyboards | `motion.systemDefault` | Never customize keyboard entrance or dismissal |
|
|
182
|
+
| Segmented controls | `motion.selectionChange` | Use native selected-segment movement and Reduce Motion behavior |
|
|
183
|
+
| Pickers | `motion.selectionChange` | Use native picker motion; selected values update immediately |
|
|
184
|
+
| Sliders, Steppers, Toggles | `motion.systemDefault`, `motion.pressFeedback` | Direct manipulation follows the gesture; no delayed or decorative response |
|
|
185
|
+
| Gauges, Progress indicators | `motion.progress` | Animate progress only while work is active; always show textual state nearby |
|
|
186
|
+
| Notifications, Live Activities | `motion.statusUpdate` | Use system delivery/update motion; detail screens show static state after open |
|
|
187
|
+
| Image views | `motion.contentReveal` | Fade or system-load media previews; keep QR/barcode image views static with `motion.none` |
|
|
188
|
+
| Text views, Labels | `motion.none` | Text updates should be readable first; use static layout changes unless status changes require attention |
|
|
189
|
+
| Web views | `motion.systemDefault` | Use system web loading/navigation behavior only |
|
|
190
|
+
| Boxes, Lockups | `motion.contentReveal` | Use only for newly revealed cards or result groups; repeated rows stay quiet |
|
|
191
|
+
|
|
192
|
+
## Composite Naming
|
|
193
|
+
|
|
194
|
+
Use HIG names for base components and product-agnostic names for app composites.
|
|
195
|
+
|
|
196
|
+
| Composite pattern | HIG composition | Default motion |
|
|
197
|
+
| --- | --- | --- |
|
|
198
|
+
| Feature card | Boxes + Text views + Buttons | `motion.contentReveal` for new content, `motion.pressFeedback` for actions, `motion.none` for passive reading |
|
|
199
|
+
| List hub | Tab bars or segmented controls + Lists and tables + Search fields | `motion.selectionChange` for tabs/segments, `motion.listUpdate` for row changes, `motion.systemDefault` for search |
|
|
200
|
+
| Prompt flow | Text views + Buttons, or Sheets when modal | `motion.contentReveal` for the next prompt, `motion.pressFeedback` for answers, `motion.presentation` when modal |
|
|
201
|
+
| Machine-readable card | Image views + Labels + Buttons | `motion.none` for the mark, `motion.contentReveal` for readiness, `motion.pressFeedback` for actions |
|
|
202
|
+
| Person or item row | Lists and tables + Image views + Labels + Buttons | `motion.pressFeedback` on open, `motion.listUpdate` on unread/read or state changes |
|
|
203
|
+
| Detail header | Toolbars + Labels + Buttons | `motion.systemDefault` toolbar motion, `motion.statusUpdate` for important changes |
|
|
204
|
+
| Limit or upgrade wall | Sheets + Text views + Buttons + Lists and tables | `motion.presentation` for sheet, `motion.progress` for capacity gauge, `motion.pressFeedback` for actions |
|
|
205
|
+
|
|
206
|
+
## Shared Screen Zones
|
|
207
|
+
|
|
208
|
+
| Zone | Position on 390 x 844 baseline | Default components |
|
|
209
|
+
| --- | --- | --- |
|
|
210
|
+
| Status area | y 0-47 | Status bars |
|
|
211
|
+
| Top bar | y 47-103 | Toolbars, Labels, Buttons |
|
|
212
|
+
| Content | y 103-742 | Scroll views, Lists and tables, Boxes, Text views |
|
|
213
|
+
| Persistent action | y 742-776 | Buttons, Segmented controls, Progress indicators |
|
|
214
|
+
| Bottom navigation | y 776-844 | Tab bars |
|
|
215
|
+
|
|
216
|
+
## View Spec Template
|
|
217
|
+
|
|
218
|
+
Use this structure for reusable iOS view specs:
|
|
219
|
+
|
|
220
|
+
1. `Labels and Copy`: toolbar title, primary/secondary buttons, empty/error copy, and user-visible strings.
|
|
221
|
+
2. `HIG Component Inventory`: table with Apple HIG component name and app usage.
|
|
222
|
+
3. `Layout and Position`: safe areas, scroll regions, persistent actions, and navigation chrome.
|
|
223
|
+
4. `Color and Typography`: semantic color tokens and Dynamic Type styles.
|
|
224
|
+
5. `Functionality`: user actions, navigation, deep links, and side effects.
|
|
225
|
+
6. `States`: loading, empty, error, disabled, permission, offline, success, modal, and destructive states.
|
|
226
|
+
7. `Data Dependencies`: local state, remote data, persisted preferences, permissions, and route parameters.
|
|
227
|
+
|
|
228
|
+
## Shared Behavior
|
|
229
|
+
|
|
230
|
+
- Use native sheets for focused scoped tasks that do not need a full route.
|
|
231
|
+
- Use alerts only for destructive, failure, or irreversible confirmation states.
|
|
232
|
+
- Use progress indicators for onboarding, import/export, matching, generation, and other ongoing work; never leave algorithmic operations visually silent.
|
|
233
|
+
- Use notifications and Live Activities for latent or time-sensitive updates.
|
|
234
|
+
- Prefer lists and tables for repeatable people, items, history, settings, and activity rows.
|
|
235
|
+
- Keep native messaging, payments, wallet, sharing, and notification flows inside platform-provided surfaces whenever available.
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# SwiftUI Clean Code and Maintainability Reference
|
|
2
|
+
|
|
3
|
+
Use this reference when implementing, reviewing, or refactoring Swift and SwiftUI app code. It favors current Swift, SwiftUI Observation, structured concurrency, testable boundaries, and reusable UI primitives.
|
|
4
|
+
|
|
5
|
+
Source references to consult when precision matters:
|
|
6
|
+
|
|
7
|
+
- Swift API Design Guidelines: https://www.swift.org/documentation/api-design-guidelines/
|
|
8
|
+
- Swift Concurrency: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/
|
|
9
|
+
- Observation in SwiftUI: https://developer.apple.com/videos/play/wwdc2023/10149/
|
|
10
|
+
- Swift Testing: https://developer.apple.com/xcode/swift-testing/
|
|
11
|
+
|
|
12
|
+
## Baseline Rules
|
|
13
|
+
|
|
14
|
+
- Prefer clarity at the call site over clever compactness.
|
|
15
|
+
- Use SwiftUI and Swift native features before adding custom architecture.
|
|
16
|
+
- Keep views declarative: views describe UI, route user intent, and observe state.
|
|
17
|
+
- Keep side effects outside `body`; use explicit actions, `.task(id:)`, services, repositories, actors, or model methods.
|
|
18
|
+
- Use Swift 6 concurrency checking for new modules when possible; make cross-task dependencies `Sendable`.
|
|
19
|
+
- Prefer `struct` value models for domain data and `@Observable` reference models for view-owned mutable UI state.
|
|
20
|
+
- Avoid force unwraps, implicitly unwrapped optionals, global mutable state, hidden singletons, and magic numbers.
|
|
21
|
+
- Keep access control tight: default to `private`, `fileprivate`, or `internal` before making APIs public.
|
|
22
|
+
|
|
23
|
+
## Architecture
|
|
24
|
+
|
|
25
|
+
Use a small, boring feature structure:
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
Feature/
|
|
29
|
+
FeatureView.swift
|
|
30
|
+
FeatureModel.swift
|
|
31
|
+
FeatureClient.swift
|
|
32
|
+
FeatureTypes.swift
|
|
33
|
+
FeatureTests.swift
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Recommended boundaries:
|
|
37
|
+
|
|
38
|
+
| Layer | Responsibility | Avoid |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| View | Layout, accessibility, transitions, forwarding actions | Business rules, networking, parsing, date math |
|
|
41
|
+
| Observable model | Screen state, user intents, task orchestration | Direct URLSession code, persistence details |
|
|
42
|
+
| Client or repository | I/O boundary and integration mapping | View-specific labels or layout decisions |
|
|
43
|
+
| Domain types | Stable values and rules | Framework-specific UI state |
|
|
44
|
+
|
|
45
|
+
Create protocols at boundaries that need mocking, replacement, or concurrency isolation. Do not create a protocol for every concrete type by default.
|
|
46
|
+
|
|
47
|
+
```swift
|
|
48
|
+
protocol ProfileClient: Sendable {
|
|
49
|
+
func profile() async throws -> Profile
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## SwiftUI State
|
|
54
|
+
|
|
55
|
+
Use the modern Observation model for new SwiftUI code.
|
|
56
|
+
|
|
57
|
+
| Need | Preferred tool |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| View-owned local value | `@State` |
|
|
60
|
+
| View-owned observable model | `@State` with `@Observable` model |
|
|
61
|
+
| Parent-owned editable value | `@Binding` |
|
|
62
|
+
| Bindings into an observable model | `@Bindable` |
|
|
63
|
+
| App-wide dependency or model | `@Environment` |
|
|
64
|
+
| Legacy ObservableObject interoperability | `@StateObject`, `@ObservedObject`, `@EnvironmentObject` only where migration requires it |
|
|
65
|
+
|
|
66
|
+
```swift
|
|
67
|
+
import Observation
|
|
68
|
+
import SwiftUI
|
|
69
|
+
|
|
70
|
+
@MainActor
|
|
71
|
+
@Observable
|
|
72
|
+
final class ProfileModel {
|
|
73
|
+
private let client: ProfileClient
|
|
74
|
+
private(set) var state: Loadable<Profile> = .idle
|
|
75
|
+
|
|
76
|
+
init(client: ProfileClient) {
|
|
77
|
+
self.client = client
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func load() async {
|
|
81
|
+
state = .loading
|
|
82
|
+
do {
|
|
83
|
+
state = .loaded(try await client.profile())
|
|
84
|
+
} catch is CancellationError {
|
|
85
|
+
state = .idle
|
|
86
|
+
} catch {
|
|
87
|
+
state = .failed(DisplayError(error))
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
struct ProfileView: View {
|
|
93
|
+
@State private var model: ProfileModel
|
|
94
|
+
|
|
95
|
+
init(client: ProfileClient) {
|
|
96
|
+
_model = State(initialValue: ProfileModel(client: client))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
var body: some View {
|
|
100
|
+
content
|
|
101
|
+
.task { await model.load() }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@ViewBuilder
|
|
105
|
+
private var content: some View {
|
|
106
|
+
switch model.state {
|
|
107
|
+
case .idle, .loading:
|
|
108
|
+
ProgressView()
|
|
109
|
+
case .loaded(let profile):
|
|
110
|
+
ProfileContent(profile: profile)
|
|
111
|
+
case .empty:
|
|
112
|
+
EmptyState()
|
|
113
|
+
case .failed(let error):
|
|
114
|
+
ErrorState(error: error, retry: { Task { await model.load() } })
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## View Composition
|
|
121
|
+
|
|
122
|
+
- Extract a subview when a block has a separate responsibility, needs its own preview, has local state, is reused, or makes the parent hard to scan.
|
|
123
|
+
- Use private computed `some View` properties for small local pieces that do not need separate identity or testing.
|
|
124
|
+
- Prefer `@ViewBuilder` and generics over `AnyView`; use `AnyView` only at true type-erasure boundaries.
|
|
125
|
+
- Keep list identity stable. Never create `UUID()` inside `body` for `ForEach` identity.
|
|
126
|
+
- Keep layout values named when they encode a product decision.
|
|
127
|
+
- Prefer semantic `Label`, `LabeledContent`, `Button`, `List`, `NavigationStack`, `ToolbarItem`, and native controls before custom compositions.
|
|
128
|
+
|
|
129
|
+
```swift
|
|
130
|
+
private enum Layout {
|
|
131
|
+
static let avatarSize: CGFloat = 48
|
|
132
|
+
static let rowSpacing: CGFloat = 12
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Reusable UI
|
|
137
|
+
|
|
138
|
+
Choose the lightest reusable primitive that solves the duplication:
|
|
139
|
+
|
|
140
|
+
| Reuse need | Tool |
|
|
141
|
+
| --- | --- |
|
|
142
|
+
| Repeated control behavior | `ButtonStyle`, `ToggleStyle`, `LabelStyle` |
|
|
143
|
+
| Repeated view decoration | `ViewModifier` |
|
|
144
|
+
| Repeated layout algorithm | Custom `Layout` |
|
|
145
|
+
| Repeated content pattern | Generic `View` with typed inputs |
|
|
146
|
+
| Repeated product state | Domain enum or value object |
|
|
147
|
+
|
|
148
|
+
Use `mobile-design-system.md` for typography, color, motion, materials, and component naming. Do not hard-code custom colors, fixed fonts, or one-off corner radii when a system token or native component applies.
|
|
149
|
+
|
|
150
|
+
```swift
|
|
151
|
+
struct PrimaryActionButtonStyle: ButtonStyle {
|
|
152
|
+
func makeBody(configuration: Configuration) -> some View {
|
|
153
|
+
configuration.label
|
|
154
|
+
.font(.headline)
|
|
155
|
+
.frame(maxWidth: .infinity)
|
|
156
|
+
.padding(.vertical, 14)
|
|
157
|
+
.background(.tint, in: RoundedRectangle(cornerRadius: 8))
|
|
158
|
+
.foregroundStyle(.white)
|
|
159
|
+
.opacity(configuration.isPressed ? 0.8 : 1)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Concurrency
|
|
165
|
+
|
|
166
|
+
- Mark UI models `@MainActor` unless they are deliberately isolated elsewhere.
|
|
167
|
+
- Use `async`/`await`, `async let`, and task groups for structured asynchronous work.
|
|
168
|
+
- Avoid `Task.detached` unless isolation from the current actor is intentional and documented.
|
|
169
|
+
- Do not block async code with semaphores, sleeps, synchronous waits, or sync-over-async adapters.
|
|
170
|
+
- Check cancellation in long-running work and avoid starting new child tasks after cancellation.
|
|
171
|
+
- Keep CPU-heavy work off the main actor, then return to the main actor for UI state updates.
|
|
172
|
+
- Use actors for shared mutable state that can be touched from multiple tasks.
|
|
173
|
+
- Make services, closures, and data crossing concurrency boundaries `Sendable` where practical.
|
|
174
|
+
|
|
175
|
+
```swift
|
|
176
|
+
actor ProfileCache {
|
|
177
|
+
private var profiles: [Profile.ID: Profile] = [:]
|
|
178
|
+
|
|
179
|
+
func profile(id: Profile.ID) -> Profile? {
|
|
180
|
+
profiles[id]
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
func store(_ profile: Profile) {
|
|
184
|
+
profiles[profile.id] = profile
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Loading, Errors, and Empty States
|
|
190
|
+
|
|
191
|
+
Model screen state explicitly. Avoid boolean soup like `isLoading`, `hasError`, `isEmpty`, and `didFinish` when those states are mutually exclusive.
|
|
192
|
+
|
|
193
|
+
```swift
|
|
194
|
+
enum Loadable<Value> {
|
|
195
|
+
case idle
|
|
196
|
+
case loading
|
|
197
|
+
case loaded(Value)
|
|
198
|
+
case empty
|
|
199
|
+
case failed(DisplayError)
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Error guidance:
|
|
204
|
+
|
|
205
|
+
- Convert low-level errors into user-safe `DisplayError` values near the UI boundary.
|
|
206
|
+
- Preserve the underlying error for logging, diagnostics, or retry policy.
|
|
207
|
+
- Always provide a recovery action when the user can do something.
|
|
208
|
+
- Treat cancellation as a normal control-flow outcome, not a user-facing failure.
|
|
209
|
+
|
|
210
|
+
## Dependencies
|
|
211
|
+
|
|
212
|
+
- Inject time, UUID generation, networking, persistence, feature flags, analytics, and notification scheduling.
|
|
213
|
+
- Prefer small dependency values or closures for simple capabilities.
|
|
214
|
+
- Prefer protocols for cross-module boundaries, external clients, and services with multiple implementations.
|
|
215
|
+
- Keep dependencies out of SwiftUI previews by providing sample clients and static fixtures.
|
|
216
|
+
|
|
217
|
+
```swift
|
|
218
|
+
struct AppClock: Sendable {
|
|
219
|
+
var now: @Sendable () -> Date
|
|
220
|
+
|
|
221
|
+
static let live = AppClock(now: Date.init)
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Naming and API Design
|
|
226
|
+
|
|
227
|
+
- Follow Swift API Design Guidelines: names should make use sites clear.
|
|
228
|
+
- Use nouns for types and values, verbs for side-effecting actions, and assertion-like names for booleans.
|
|
229
|
+
- Name protocols by role or capability, such as `ProfileClient`, `ImageLoading`, or `ProgressReporting`.
|
|
230
|
+
- Avoid abbreviations unless they are standard in the domain.
|
|
231
|
+
- Document non-obvious public APIs and computed properties that are not O(1).
|
|
232
|
+
|
|
233
|
+
```swift
|
|
234
|
+
// Good
|
|
235
|
+
func removeContact(id: Contact.ID)
|
|
236
|
+
var hasUnreadUpdates: Bool
|
|
237
|
+
|
|
238
|
+
// Avoid
|
|
239
|
+
func process(_ value: String)
|
|
240
|
+
var flag: Bool
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Accessibility and Localization
|
|
244
|
+
|
|
245
|
+
- Support Dynamic Type, including larger accessibility sizes.
|
|
246
|
+
- Do not use color as the only signal for state, selection, errors, or limits.
|
|
247
|
+
- Add accessibility labels and values when visible text is insufficient.
|
|
248
|
+
- Keep tappable targets at least 44 x 44 pt.
|
|
249
|
+
- Localize user-facing strings and use `FormatStyle` for dates, numbers, currencies, and lists.
|
|
250
|
+
- Test important screens with VoiceOver, Reduce Motion, Increase Contrast, dark mode, and long localized strings.
|
|
251
|
+
|
|
252
|
+
```swift
|
|
253
|
+
Text(event.lastUpdatedAt, format: .dateTime.month().day().hour().minute())
|
|
254
|
+
.font(.subheadline)
|
|
255
|
+
.foregroundStyle(.secondary)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Testing
|
|
259
|
+
|
|
260
|
+
Use Swift Testing for new unit and integration-style tests. Keep XCTest where it already exists, for UI tests, or when a framework still requires it.
|
|
261
|
+
|
|
262
|
+
Test priority:
|
|
263
|
+
|
|
264
|
+
1. Domain value behavior and business rules.
|
|
265
|
+
2. Observable model state transitions.
|
|
266
|
+
3. Service mapping, retry, cancellation, and error conversion.
|
|
267
|
+
4. Accessibility labels and critical UI states through previews or UI tests.
|
|
268
|
+
|
|
269
|
+
```swift
|
|
270
|
+
import Testing
|
|
271
|
+
|
|
272
|
+
@MainActor
|
|
273
|
+
@Test("Profile load stores loaded state")
|
|
274
|
+
func profileLoadStoresLoadedState() async throws {
|
|
275
|
+
let client = StubProfileClient(result: .success(.sample))
|
|
276
|
+
let model = ProfileModel(client: client)
|
|
277
|
+
|
|
278
|
+
await model.load()
|
|
279
|
+
|
|
280
|
+
#expect(model.state == .loaded(.sample))
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Testing rules:
|
|
285
|
+
|
|
286
|
+
- Make tests deterministic by injecting clocks, IDs, clients, and fixtures.
|
|
287
|
+
- Test cancellation and failure paths, not only success paths.
|
|
288
|
+
- Keep preview data realistic and privacy-safe.
|
|
289
|
+
- Prefer one clear behavior per test.
|
|
290
|
+
- Use parameterized tests for repeated rules.
|
|
291
|
+
|
|
292
|
+
## File and Module Organization
|
|
293
|
+
|
|
294
|
+
- Keep one primary type per file unless helper types are private and tiny.
|
|
295
|
+
- Put protocol conformances in focused extensions.
|
|
296
|
+
- Keep generated code, preview fixtures, and test helpers separate from app code.
|
|
297
|
+
- Avoid giant shared utility files. Utilities should have a narrow domain and clear owner.
|
|
298
|
+
- Keep feature modules independent of UI where possible; UI should depend on domain and clients, not the reverse.
|
|
299
|
+
- Use SwiftFormat or SwiftLint only when rules reinforce readability and team consistency.
|
|
300
|
+
|
|
301
|
+
## Review Checklist
|
|
302
|
+
|
|
303
|
+
- Does the view body read as layout rather than business logic?
|
|
304
|
+
- Are state transitions modeled with explicit types?
|
|
305
|
+
- Are dependencies injected and testable?
|
|
306
|
+
- Are async tasks cancellable and actor isolation clear?
|
|
307
|
+
- Are cross-task values safely `Sendable` or isolated?
|
|
308
|
+
- Are names clear at the call site?
|
|
309
|
+
- Are colors, fonts, spacing, and motion aligned with the design system?
|
|
310
|
+
- Are accessibility, localization, loading, empty, and error states covered?
|
|
311
|
+
- Are tests deterministic and focused on behavior?
|