{"slug":"solo-swiftui-design-system","title":"solo-swiftui-design-system","summary":"Build and hold a SwiftUI design system — a 12-column grid, spacing/type/radius/motion scales, surface levels, a component gallery, and the guards that stop it drifting back. Use when the user says \"сделай по сетке\", \"дизайн-система\", \"разъезжается вёрстка\", \"магические числа в pa","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-07T19:02:23.282218Z","repo":{"url":"https://github.com/fortunto2/solo-factory","stars":18,"forks":3,"license":"MIT","updatedAt":"2026-10-01T06:11:21Z"},"bodyHtml":"<hr>\n<h2>name: solo-swiftui-design-system\ndescription: Build and hold a SwiftUI design system — a 12-column grid, spacing/type/radius/motion scales, surface levels, a component gallery, and the guards that stop it drifting back. Use when the user says \"сделай по сетке\", \"дизайн-система\", \"разъезжается вёрстка\", \"магические числа в padding\", \"design tokens\", \"grid 12\", \"нужен storybook/каталог компонентов\", or when a screen's spacing and fonts were each chosen once and never against each other. Do NOT use for SwiftUI correctness, state flow or Instruments profiling (that is the swiftui-expert skill), or for shipping to TestFlight (solo-ios-release).\nlicense: MIT\nmetadata:\nauthor: fortunto2\nversion: \"1.0.0\"\nopenclaw:\nemoji: \"\uD83D\uDCD0\"</h2>\n<h1>swiftui-design-system — one scale, and the guards that keep it</h1>\n<p>A design system is not a colour file. It is <strong>four scales, one grid, one\ncatalogue and one tripwire</strong> — and the tripwire is what makes it survive the\nnext feature. Everything here is native SwiftUI: no design-system framework is\nworth taking on, because the platform is already built for this (Environment +\nstyle protocols + <code>containerRelativeFrame</code>).</p>\n<p><strong>Apple ships its own SwiftUI skills inside Xcode 27</strong> — invalidation, <code>ForEach</code>\nidentity, <code>@Observable</code>, Liquid Glass — as plain markdown any agent can read\nwith no Xcode running. On anything about correctness or update cost they\noutrank this file, and they say so in their own header. Where they are, what\nthey cover, and the rules that most often hit real code:\n<strong><code>references/apple-xcode-skills.md</code></strong>.</p>\n<p>Reach for this when the symptom is <em>\"the screens look related and never line\nup\"</em>. Measure before you believe it — see step 1.</p>\n<h2>Workflow</h2>\n<h3>1. Count what is actually there</h3>\n<p>Never start from taste. Start from the inventory, because the number is the\nargument:</p>\n<pre><code>V=path/to/Views\ngrep -rhoE '\\.padding\\((\\.[a-z]+, )?[0-9]+(\\.[0-9])?\\)' $V | grep -oE '[0-9.]+' | sort -n | uniq -c | sort -rn\ngrep -rhoE 'spacing: [0-9]+(\\.[0-9])?' $V | sort | uniq -c | sort -rn\ngrep -rhoE 'cornerRadius: [0-9]+(\\.[0-9])?' $V | sort | uniq -c | sort -rn\ngrep -rhoE '\\.font\\(\\.[a-zA-Z0-9]+\\)|\\.font\\(\\.system\\([^)]*\\)\\)' $V | sort | uniq -c | sort -rn\ngrep -rhoE '\\.spring\\(response: [0-9.]+, dampingFraction: [0-9.]+\\)' $V | sort -u | wc -l\n</code></pre>\n<p>A real app measured this way: <strong>27 padding values, 19 spacings, 23 radii, 48\nfont spellings, 21 springs</strong> — including <code>.system(size: 17.5)</code>, <code>13.5</code>, <code>12.5</code>.\nHalf a point is invisible alone and lethal in a set: it is exactly the amount by\nwhich two labels fail to look like the same label.</p>\n<h3>2. Write the scales — namespace enums, one file each</h3>\n<pre><code>Views/Design/\n  Space.swift       4-point steps: hair(2) xs sm md lg xl xxl xxxl huge + touchTarget(44)\n  Grid.swift        12 columns, gutter, margin, gridSpan(_:), gridMargins(), GridOverlay\n  Typography.swift  8 steps, each a platform TextStyle + Metric for sizes a font can't set\n  Radii.swift       5 radii + shape(_:) — .continuous, always\n  Motion.swift      6 curves + the never-repeatForever rule\n  Surface.swift     3–4 levels (console/panel/inset/field), one recipe each\n</code></pre>\n<p>Enums over structs: compile-time names, zero runtime, no instance to thread.\n<strong>Reach for <code>@Entry var theme</code> in the Environment only when there is a second\ntheme</strong> (white-label, per-brand, light/dark that is not the system's). Until\nthen a theme object is one indirection buying nothing.</p>\n<p>Two rules that decide the arguments in step 3:</p>\n<ul>\n<li><strong>Every type step is a platform text style</strong> — <code>Font.system(.subheadline, weight:)</code>, not <code>.system(size: 15)</code>. Fixed sizes never grow with Dynamic Type;\nin the measured app 78 of them didn't. For sizes a font cannot set (an icon's\nbox, a ring's diameter) use <code>@ScaledMetric(relativeTo:)</code> over a <code>Metric</code>\nconstant.</li>\n<li><strong>The unit of layout is the column, not the point.</strong> <code>containerRelativeFrame( .horizontal, count: 12, span: 4, spacing: gutter)</code> is iOS 17+ and is the\nplatform's own grid arithmetic — no <code>GeometryReader</code>, no percentages.</li>\n</ul>\n<h3>3. Migrate mechanically, with the rounding rule written down</h3>\n<p>Hand-editing hundreds of literals is where a migration dies — half done, half\nnot, and nobody can say which half. Write a codemod, keep it in the repo, and\nlet it carry the rule:</p>\n<pre><code># nearest 4-point step, TIES GO UP (6→8, not 4): rounding down tightens a\n# third of the app by two points at once, and a snug layout is the one that\n# breaks. Only padding / spacing / cornerRadius / fonts in 9…26pt —\n# never frame, offset, lineWidth or a shadow radius.\nstep, name = min(SCALE, key=lambda p: (abs(p[0] - value), -p[0]))\n</code></pre>\n<p><strong>Full rule, the skip list, the spring and font tables: <code>references/codemod.md</code>.</strong>\nRead it when actually migrating; one measured run was 991 substitutions in 31\nfiles, dry-run first, build after each family.</p>\n<h3>4. Style protocols, not modifiers sprinkled per call site</h3>\n<p>SwiftUI's extension points are the system's spine — use them before inventing\n<code>.myButton()</code>:</p>\n<pre><code>struct PressableStyle: ButtonStyle {           // one feel for every control\n    func makeBody(configuration: Configuration) -&gt; some View {\n        configuration.label\n            .scaleEffect(configuration.isPressed ? 0.97 : 1)\n            .animation(configuration.isPressed ? Motion.press : Motion.release,\n                       value: configuration.isPressed)\n    }\n}\n</code></pre>\n<p><code>LabelStyle</code>, <code>ToggleStyle</code>, <code>ProgressViewStyle</code>, <code>MenuStyle</code> the same way; a\n<code>ViewModifier</code> + <code>extension View</code> for what has no protocol (<code>.surface(.inset)</code>).</p>\n<h3>5. Build the catalogue — and make it cheap to look at</h3>\n<p>Two doors, answering different questions:</p>\n<pre><code>make design       # ImageRenderer → PNG sheets from a test: no launch, no taps, ~3s\nmake design-app   # the same gallery in the simulator: glass, blur, motion\n… launch &lt;app&gt; -designGrid YES   # the 12 columns over the REAL screens\n</code></pre>\n<p><code>ImageRenderer</code> inside an XCTest is the whole storybook you need — a gallery\nview rendered to <code>docs/previews/*.png</code>, one file per sheet, no navigation and no\nexternal dependency. <strong>It does not draw materials</strong>: <code>.ultraThinMaterial</code> and\n<code>glassEffect</code> come out empty, so glass reads flat there. Geometry is exact,\nwhich is what the sheets are for.</p>\n<p>The grid overlay over the <em>real</em> app is the only thing that proves two screens\nagree; a gallery only proves one screen is tidy.</p>\n<p><strong>The writer, the debug flag, the localisation trap and when to reach for\nswift-snapshot-testing: <code>references/catalogue.md</code>.</strong></p>\n<h3>6. Guard it, or it comes back</h3>\n<p>Three layers, cheapest first:</p>\n<ol>\n<li><strong>pre-commit grep on added lines</strong> (warn, not fail): a numeric\n<code>.padding(12)</code>, <code>spacing: 6</code>, <code>cornerRadius: 18</code>, <code>.font(.system(size: 13))</code>.\nDiff-scoped and warning-only is the right calibration — a whole-tree lint at\nfail severity breaks on inherited debt and gets bypassed, and a bypassed hook\nchecks nothing.</li>\n<li><strong>tests on the arithmetic</strong>: 12 columns + 11 gutters + 2 margins == the\nscreen; <code>span(6) * 2 + gutter == width</code>; every space step divisible by 4\nexcept the one deliberate half step.</li>\n<li><strong>snapshot sheets in the repo</strong> — a reviewer sees the scale change as an\nimage diff.</li>\n</ol>\n<h2>Gotchas</h2>\n<ul>\n<li><strong><code>containerRelativeFrame</code> measures the container, not its content.</strong> Apply\n<code>gridMargins()</code> first, or every span is a margin too wide and nothing lines\nup with anything.</li>\n<li><strong>A <code>Sendable</code> warning on <code>static let</code> tokens</strong> in Swift 6: an enum of\n<code>static let CGFloat</code> is fine; a struct holding <code>UserDefaults</code> needs\n<code>@unchecked Sendable</code> with a one-line reason.</li>\n<li><strong>Glass cannot sample glass.</strong> Two blurred surfaces side by side each sample\nwhat is behind them and read as unrelated panes. On iOS 26 wrap a row of them\nin <code>GlassEffectContainer(spacing:)</code> and give morphing pairs a\n<code>.glassEffectID(_:in:)</code>; below 26 fall back to <code>.ultraThinMaterial</code> in one\nplace, not per screen.</li>\n<li><strong><code>compositingGroup()</code> + a zero shadow is still an offscreen pass.</strong> Apply\nthe lift only where there is a shadow to draw.</li>\n<li><strong>Never <code>repeatForever</code>.</strong> A UI that never goes idle hangs everything that\nwaits for idle: accessibility snapshots, UI automation, VoiceOver. Use\n<code>.repeatCount(n)</code> and honour <code>\\.accessibilityReduceMotion</code>.</li>\n<li><strong>A debug surface needs a testable flag.</strong> <code>-designGallery YES</code> from <code>simctl</code>\nlands in the argument domain, so one <code>UserDefaults.bool(forKey:)</code> read covers\nit — but wrap it in a small injectable type that is false in Release, or the\nflag ships.</li>\n<li><strong>A scalar threaded by hand through call sites is invisible to grep.</strong> In the\nmeasured app a wheel's vertical offset was written in three places; two moved\nonto the shared centre and the third did not, so the drawing and the single-tap\nhit test disagreed by 20 points with nothing on screen to say so. Pass one\ngeometry <em>value</em> — then a call site that forgets it does not compile.</li>\n</ul>\n<h2>What is coming (and what already works)</h2>\n<ul>\n<li><strong>iOS 26 / Swift 6.2 — today.</strong> Liquid Glass (<code>glassEffect</code>,\n<code>GlassEffectContainer</code>, <code>.buttonStyle(.glass)</code>), <code>@Entry</code> for environment\ntokens with no <code>EnvironmentKey</code> boilerplate, <code>ToolbarSpacer</code>,\n<code>backgroundExtensionEffect()</code>, <code>scrollEdgeEffectStyle</code>.</li>\n<li><strong>iOS 27 / Swift 6.4 (WWDC26 → 2027).</strong> <code>ContentBuilder</code> collapses the\ncontainer overloads that cause <em>\"unable to type-check this expression in\nreasonable time\"</em> — and it helps when built with the new Xcode regardless of\ndeployment target. <code>.reorderable()</code> in any container (not just <code>List</code>), swipe\nactions outside <code>List</code>, toolbar overflow priorities, <code>@State</code> as a macro with\nlazy <code>@Observable</code> init (back-deployed to iOS 17). <strong>Resizable iPhone apps</strong> is\nthe one that touches a design system directly: baked-in sizes stop being safe,\nso snapshot at several widths.</li>\n</ul>\n<h2>Apple's own skills, and the rest of the field</h2>\n<p>Xcode ships agent skills in the toolchain — plain <code>SKILL.md</code> folders, so they\nwork in any agent, not only Xcode's assistant.</p>\n<pre><code>make apple-skills          # solo-factory: export into ~/.agents/skills, diffed\nmake apple-skills-check    # what it would bring, without writing\n</code></pre>\n<p><strong>Xcode 26.6 exports nothing</strong> — the <code>agent</code> tool is there and answers <em>\"No\nskills available to export\"</em>, so the script says that plainly rather than look\nbroken. <strong>Xcode 27.0 beta 2 exports ten</strong>: <code>swiftui-specialist</code>,\n<code>swiftui-whats-new-27</code>, <code>uikit-app-modernization</code>, <code>modernize-tests</code>,\n<code>audit-xcode-security-settings</code>, <code>adopt-c-bounds-safety</code>, <code>device-interaction</code>,\n<code>app-intents-specialist</code>, <code>app-intents-whats-new-27</code> and\n<code>building-document-based-swiftui-applications</code>. The names are not stable across\nversions — four of the seven guessed from the 26.6 release notes came back\nspelled differently — so read the export rather than a list.</p>\n<p>Re-run after every Xcode update: these track the SDK, and a stale \"what's new\"\nskill is worse than none. A beta installed alongside the release is not the\nactive toolchain, and <code>xcode-select</code> is machine-wide — scope one run instead:\n<code>DEVELOPER_DIR=\"/Applications/Xcode-beta.app/Contents/Developer\" make apple-skills</code>.\nIf the export names the toolchain instead, Xcode → Settings → Locations →\nCommand Line Tools points at the wrong Xcode.</p>\n<p><code>device-interaction</code> is the one that matters for a design system: it drives a\nreal device or simulator — screenshots, view hierarchy, synthesised taps — which\ncloses the loop a build tool alone cannot (write layout → build → look at it →\ncorrect it) without a human running the walk.</p>\n<p>Community skills worth reading <strong>before</strong> installing — a skill is injected into\nthe assistant's context and changes how it writes your code:</p>\n<table>\n<thead>\n<tr>\n<th>Where</th>\n<th>Why</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>twostraws/Swift-Agent-Skills</code></td>\n<td>curated index; start here</td>\n</tr>\n<tr>\n<td><code>twostraws/SwiftUI-Agent-Skill</code> (<code>swiftui-pro</code>)</td>\n<td>aimed at the mistakes LLMs actually make: navigation, layout, state, VoiceOver, deprecated APIs</td>\n</tr>\n<tr>\n<td><code>AvdLee/SwiftUI-Agent-Skill</code></td>\n<td>the architecture to copy — references loaded on demand, so deep context costs nothing until asked for. Also a maintenance skill that refreshes the deprecated-API list after each release</td>\n</tr>\n<tr>\n<td><code>Dimillian/Skills</code></td>\n<td><code>swiftui-liquid-glass</code>, <code>swiftui-view-refactor</code>, <code>swiftui-performance-audit</code></td>\n</tr>\n<tr>\n<td><code>dpearson2699/swift-ios-skills</code></td>\n<td>86 skills on iOS 26+ — <strong>PolyForm Perimeter licence, not MIT</strong>; read it before commercial use</td>\n</tr>\n</tbody>\n</table>\n<p>Design-system repos worth reading rather than depending on: <strong>DSKit</strong> (organised\nfor agents — generated docs link every component to its source, snapshots and\nusage), <strong>OversizeUI</strong> (semantic colours, Dynamic Type, spacing scale),\n<strong>design-foundation</strong> (MIT, Swift 6 concurrency-safe), <strong>ouds-ios</strong> (corporate\nscale, strong accessibility).</p>\n<h2>Don't</h2>\n<ul>\n<li><strong>Don't add a design-system framework.</strong> Environment + style protocols + the\ngrid API cover it; a framework on top mostly fights the layout system.</li>\n<li><strong>Don't name colours <code>blue500</code>.</strong> Semantic names (<code>surfaceElevated</code>,\n<code>textSecondary</code>) survive a re-skin; a palette index turns one into a\nfind-and-replace across the app.</li>\n<li><strong>Don't fold weight into the type scale.</strong> <code>isSelected ? .semibold : .regular</code>\nis a step plus <code>.fontWeight()</code> on top — folding it in is how a scale of eight\nbecomes a scale of sixteen.</li>\n<li><strong>Don't chase the photo grid onto the interface gutter.</strong> A wall of images\nwants 1–2pt between tiles; keep it <em>on</em> the twelve columns (a tile is a third\nof the width) and let the spacing be its own.</li>\n<li><strong>Don't ship aliases.</strong> <code>Brand.tabRadius = Radii.lg</code> reads as tidy and puts\ntwo spellings of 16pt in one file within a week.</li>\n<li><strong>Don't measure a render or a build on a loaded machine.</strong> Check <code>vm.loadavg</code>\nfirst; three \"regressions\" in one project were the laptop.</li>\n</ul>\n","files":[{"path":"references/apple-xcode-skills.md","sizeBytes":6914,"isText":true},{"path":"references/catalogue.md","sizeBytes":5326,"isText":true},{"path":"references/codemod.md","sizeBytes":4397,"isText":true},{"path":"scripts/design_migrate.py","sizeBytes":11247,"isText":true},{"path":"SKILL.md","sizeBytes":16137,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-13T07:21:32.335847Z","sha256":"1A452A033AB0F41CF5F1E1E97DBFD5CF63945888F20A83EDE8B730A5BCC94ED4","sizeBytes":20940},"review":null,"source":{"repositoryUrl":"https://github.com/fortunto2/solo-factory","path":"skills/swiftui-design-system","license":"MIT","commit":"826bb495cb1b5cde1b2b09ec57fbad5af582feb7","subtreeSha":"239CF036831CD9FEE8B88D129F66E87AEB6D831E6067C8C5D25D5FAD5167B2B7","lastSyncedAt":"2026-10-04T15:23:32.039794Z"},"reviewedAt":"2026-09-13T07:29:53.008822Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/fortunto2/solo-factory/tree/main/skills/swiftui-design-system"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install fortunto2-solo-factory@llmmart"},{"target":"git","command":"git clone https://github.com/fortunto2/solo-factory.git"}]}