swiftship/internal/skills/data/always/design-system/SKILL.md
Design system rules: AppTheme token pattern, Color(hex:) extension, Colors/Fonts/Spacing/Style enums, SF Symbols, typography tokens. Use when defining colors, spacing, fonts, or any visual design tokens, or when creating/editing AppTheme. Triggers: AppTheme, Color, .primary, .secondary, spacing, cornerRadius, font, SF Symbol.
npx skillsauth add abdullah4ai/apple-dev-docs design-systemInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Every color, font, and spacing value in the app MUST come from AppTheme. No exceptions.
Before writing ANY view code, verify:
AppTheme.Colors have a token for the color I need? If not, add one.AppTheme.Fonts have a token for the font I need? If not, add one.AppTheme.Spacing for padding/spacing? If not, switch to it.These patterns are BANNED everywhere in feature views. Violations MUST be caught and fixed.
// BANNED — hardcoded colors
.foregroundStyle(.white) // use AppTheme.Colors.textPrimary
.foregroundStyle(.white.opacity(0.8)) // use AppTheme.Colors.textSecondary
.foregroundStyle(.white.opacity(0.6)) // use AppTheme.Colors.textTertiary
.foregroundStyle(.black) // use AppTheme.Colors.textPrimary
.foregroundStyle(Color.red) // define AppTheme.Colors.error or semantic token
.foregroundStyle(Color.blue) // define AppTheme.Colors.accent or semantic token
.background(.blue) // use AppTheme.Colors.* token
.background(Color(hex: "FF0000")) // define in AppTheme.Colors, reference the token
.tint(.white) // use AppTheme.Colors.* token
// BANNED — hardcoded fonts
.font(.system(size: 48)) // use AppTheme.Fonts.* token
.font(.system(size: 64)) // use AppTheme.Fonts.* token
.font(.system(.largeTitle, design: .rounded, weight: .bold)) // define in AppTheme.Fonts
.font(.title2) // use AppTheme.Fonts.title2
.font(.caption) // use AppTheme.Fonts.caption
.font(.headline) // use AppTheme.Fonts.headline
// BANNED — hardcoded spacing
.padding(20) // use AppTheme.Spacing.*
.padding(.horizontal, 12) // use AppTheme.Spacing.*
VStack(spacing: 10) // use AppTheme.Spacing.*
// CORRECT — always use AppTheme tokens
.foregroundStyle(AppTheme.Colors.textPrimary) // system adaptive — auto-adapts to appearance
.foregroundStyle(AppTheme.Colors.textSecondary)
.font(AppTheme.Fonts.title2)
.font(AppTheme.Fonts.caption)
.padding(AppTheme.Spacing.md)
.padding(.horizontal, AppTheme.Spacing.sm)
VStack(spacing: AppTheme.Spacing.sm)
.background(AppTheme.Colors.background) // custom palette hex — brand identity
Every app MUST use a centralized theme with nested enums for Colors, Fonts, and Spacing. Do NOT use a flat enum with top-level static properties.
// REQUIRED — always use nested enums
import SwiftUI
enum AppTheme {
enum Colors {
// Brand/theme colors — custom hex from palette
static let primary = Color(hex: "...")
static let secondary = Color(hex: "...")
static let accent = Color(hex: "...")
static let background = Color(hex: "...")
static let surface = Color(hex: "...")
// Text colors — REQUIRED — use UIKit adaptive colors
// These auto-adapt to the locked appearance (Dark or Light).
static let textPrimary = Color(.label)
static let textSecondary = Color(.secondaryLabel)
static let textTertiary = Color(.tertiaryLabel)
}
enum Fonts {
static let largeTitle = Font.system(.largeTitle, design: .rounded, weight: .bold)
static let title = Font.system(.title, design: .rounded, weight: .bold)
static let title2 = Font.system(.title2, design: .rounded, weight: .semibold)
static let title3 = Font.system(.title3, design: .rounded, weight: .semibold)
static let headline = Font.system(.headline, design: .rounded)
static let body = Font.system(.body, design: .rounded)
static let callout = Font.system(.callout, design: .rounded)
static let subheadline = Font.system(.subheadline, design: .rounded)
static let footnote = Font.system(.footnote, design: .rounded)
static let caption = Font.system(.caption, design: .rounded)
static let caption2 = Font.system(.caption2, design: .rounded)
// Icon-specific sizes — use when SF Symbols need a size that doesn't match a text style
static let heroIcon = Font.system(size: 80, weight: .regular, design: .rounded)
static let bulletIcon = Font.system(size: 6, weight: .regular, design: .rounded)
}
enum Spacing {
static let xs: CGFloat = 4
static let sm: CGFloat = 8
static let md: CGFloat = 16
static let lg: CGFloat = 24
static let xl: CGFloat = 40
}
enum Style {
static let cornerRadius: CGFloat = 12
static let cardCornerRadius: CGFloat = 16
}
}
// FORBIDDEN — never use flat structure
enum AppTheme {
static let accentColor = Color.blue // wrong
static let spacing: CGFloat = 8 // wrong
}
Reference as: AppTheme.Colors.accent, AppTheme.Fonts.headline, AppTheme.Spacing.md
The Fonts enum MUST exist in every AppTheme. It defines the app's typography tokens using the font design from the plan.
Rules:
.largeTitle, .title, .headline, .body, .captionfontDesign (rounded, serif, monospaced, default) via Font.system(.style, design: .rounded).font(.title2) or .font(.headline) in views — always AppTheme.Fonts.title2.font(.system(size: N)) inline — it opts out of Dynamic Type and violates the token ruleAppTheme.Fonts (e.g. heroIcon, bulletIcon) — never use .font(.system(size: N)) inlineEvery AppTheme MUST define text color tokens using UIKit's adaptive system colors. These automatically adapt to the locked appearance mode (Dark or Light) set via Info.plist, so text is always legible against the palette background.
// REQUIRED — UIKit adaptive text colors
static let textPrimary = Color(.label) // white in Dark, black in Light
static let textSecondary = Color(.secondaryLabel) // adapts opacity per appearance
static let textTertiary = Color(.tertiaryLabel) // adapts opacity per appearance
Do NOT use static colors for text — no Color.white, Color.black, Color.primary, or .opacity() on raw colors. The appearance lock ensures UIKit adaptive colors pick the correct variant automatically.
| Instead of | Use |
|---|---|
| .foregroundStyle(.white) | AppTheme.Colors.textPrimary |
| .foregroundStyle(.black) | AppTheme.Colors.textPrimary |
| .foregroundStyle(.white.opacity(0.8)) | AppTheme.Colors.textSecondary |
| .foregroundStyle(.white.opacity(0.6)) | AppTheme.Colors.textTertiary |
| .foregroundStyle(.secondary) | AppTheme.Colors.textSecondary |
| .foregroundStyle(Color.primary) | AppTheme.Colors.textPrimary |
macOS note: On macOS, use Color(.labelColor), Color(.secondaryLabelColor), Color(.tertiaryLabelColor) instead — AppKit uses different names than UIKit. For multi-platform shared code, use #if canImport(UIKit) / #if canImport(AppKit) to select the correct initializer.
Image(systemName: "symbol.name").symbolRenderingMode(.hierarchical) or .symbolRenderingMode(.palette) for visual depthEvery app MUST define a Color(hex:) initializer in AppTheme.swift so palette hex values can be used:
extension Color {
init(hex: String) {
let hex = hex.trimmingCharacters(in: .init(charactersIn: "#"))
let scanner = Scanner(string: hex)
var rgbValue: UInt64 = 0
scanner.scanHexInt64(&rgbValue)
self.init(
red: Double((rgbValue & 0xFF0000) >> 16) / 255.0,
green: Double((rgbValue & 0x00FF00) >> 8) / 255.0,
blue: Double(rgbValue & 0x0000FF) / 255.0
)
}
}
When the app has appearance switching (dark/light/system), also define:
iOS / tvOS / visionOS (UIKit available):
extension Color {
init(light: String, dark: String) {
self.init(uiColor: UIColor { traits in
traits.userInterfaceStyle == .dark ? UIColor(Color(hex: dark)) : UIColor(Color(hex: light))
})
}
}
macOS (AppKit, no UIKit):
extension Color {
init(light: String, dark: String) {
self.init(nsColor: NSColor(name: nil, dynamicProvider: { appearance in
let isDark = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
return isDark ? NSColor(Color(hex: dark)) : NSColor(Color(hex: light))
}))
}
}
Multi-platform shared code — use #if canImport:
extension Color {
init(light: String, dark: String) {
#if canImport(UIKit)
self.init(uiColor: UIColor { traits in
traits.userInterfaceStyle == .dark ? UIColor(Color(hex: dark)) : UIColor(Color(hex: light))
})
#elseif canImport(AppKit)
self.init(nsColor: NSColor(name: nil, dynamicProvider: { appearance in
let isDark = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
return isDark ? NSColor(Color(hex: dark)) : NSColor(Color(hex: light))
}))
#endif
}
}
When the app does NOT support both light and dark appearances (no dark-mode rule key), the pipeline automatically locks appearance via Info.plist based on palette brightness:
UIUserInterfaceStyle is set to Dark. System chrome (status bar, navigation bars, alerts, sheets, pickers) renders in dark mode to match the app's dark background.UIUserInterfaceStyle is set to Light.dark-mode rule key: No lock — the app supports adaptive light/dark/system via a user preference picker.This is handled at the XcodeGen project generation level — do NOT use .preferredColorScheme() for this purpose. The Info.plist approach ensures the entire app (including system chrome, alerts, and sheets) respects the locked appearance, not just SwiftUI views.
Text colors auto-adapt: Because the appearance is locked at the system level, UIKit adaptive colors (Color(.label), Color(.secondaryLabel), Color(.tertiaryLabel)) automatically pick the correct variant. Dark-locked apps get white text; light-locked apps get black text. No manual color switching needed.
When the app supports dark mode (dark-mode in rule_keys with Color(light:dark:) adaptive tokens), the pipeline omits these keys and the app follows the system appearance.
Color(hex:) with palette values — these are the app's visual identityColor(.label), etc.) — these auto-adapt to the appearance lock.blue, .orange, .white, .black directly in viewsAppTheme.Spacing constants throughout — never raw numeric valuesEvery list or collection MUST have an empty state. Use ContentUnavailableView (iOS 17+) for a polished look:
if items.isEmpty {
ContentUnavailableView(
"No Notes Yet",
systemImage: "note.text",
description: Text("Tap + to create your first note")
)
} else {
// Show the list
}
Map the design surfaces token to SwiftUI materials:
.ultraThinMaterial (modern/translucent).regularMaterial (depth/layers)Color from palette (clean/opaque)Always specify presentationDetents on .sheet:
.height(N) (calculate based on content).medium.large.sheet / .fullScreenCover for creation formsUse subtle, purposeful animations for state changes and list mutations:
withAnimation(.spring) {
item.isComplete.toggle()
}
.transition(.opacity.combined(with: .scale))
.contentTransition(.numericText())
.animation(.default, value: selectedFilter)
Rules:
withAnimation(.spring) for toggle/complete state changes.transition(.opacity.combined(with: .scale)) for list add/remove.spring and .default curves onlytools
Apple platform skill for docs, WWDC lookup, App Store Connect work, and SwiftUI app generation. Use repo-local `node cli.js` for Apple docs and WWDC search, `appledev store` for App Store Connect workflows, and `appledev build` for app scaffolding or fix loops on macOS. USE WHEN: Apple APIs, WWDC sessions, TestFlight/App Store tasks, or building/fixing Apple-platform apps. DON'T USE WHEN: non-Apple platforms, generic backend work, or general web research. EDGE CASES: docs-only queries use `node cli.js` in this repo, not `appledev`; release workflows use `appledev store`; app scaffolding uses `appledev build`; rules-only requests can read `references/ios-rules/` or `references/swiftui-guides/` progressively without invoking binaries.
tools
All-in-one Apple developer skill with three integrated tools shipped as a single unified binary. (1) Documentation search across Apple frameworks, symbols, and 1,267 WWDC sessions from 2014-2025. No credentials needed. (2) App Store Connect CLI with 120+ commands covering builds (find/wait/upload), TestFlight, pre-submission validate, submissions, signing, subscriptions (family-sharable), IAP, analytics, Xcode Cloud, metadata workflows, release pipeline dashboard, insights, win-back offers, promoted purchases, product pages, nominations, accessibility declarations, pre-orders, pricing filters, localizations update, diff, webhooks with local receiver, workflow automation, and more. Requires App Store Connect API key. (3) Multi-platform app builder (iOS/watchOS/tvOS/iPad/macOS/visionOS) that generates complete Swift/SwiftUI apps from natural language with auto-fix, simulator launch, interactive chat mode, and open-in-Xcode. Requires an LLM API key and Xcode. Includes 38 iOS development rules and 12 SwiftUI best practice guides for Liquid Glass, navigation, state management, and modern APIs. All three tools ship as one binary (appledev). USE WHEN: Apple API docs, App Store Connect management, WWDC lookup, or building iOS/watchOS/tvOS/macOS/visionOS apps from scratch. DON'T USE WHEN: non-Apple platforms or general coding.
testing
watchOS complications: WidgetKit complication families, accessory sizes, timeline providers for watch face. Use when implementing watchOS-specific patterns related to widgets.
development
watchOS haptic feedback: WKInterfaceDevice preset haptic types for wrist-based feedback. Use when implementing watchOS-specific patterns related to haptics.