Apple design
Skill markdavidgan/apple-dev-skills/platforms/claude/skills/apple-design
Apple platform development skills for Claude Code, Cursor, Kimi Code, Antigravity, Codex CLI, and Agy.
npx -y skills add markdavidgan/apple-dev-skills --skill apple-designAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Apple platform design system, iOS 26 & macOS 26 Liquid Glass, design tokens, and accessibility-aware previews. Use when building or reviewing SwiftUI views, defining a theme or design tokens, applying Liquid Glass, organizing asset catalogs, or improving visual consistency. Trigger on "design system", "theme", "design tokens", "Liquid Glass", "glassEffect", "SwiftUI styling", or "make the UI consistent".
SKILL.md
16.8 KB, as published. Nobody here has run it
iOS Design
SwiftUI design system patterns, iOS 26 Liquid Glass effects, and accessibility best practices. Apply these patterns to all UI code.
Design System Architecture
Theme.swift Pattern
Centralize design tokens in a theme enum. Apps extend a shared base theme for app-specific expression.
// Shared package: AppTheme.swift
public enum AppTheme {
// Foundation: Backgrounds
public static var canvas: Color {
Color(light: Color(hex: "#FAF9F6"), dark: Color(hex: "#0D0D0F"))
}
// Action: Primary color
public static let actionPrimary = Color(hex: "#7BA7BC")
// Spacing system
public enum Spacing {
public static let xs: CGFloat = 8
public static let md: CGFloat = 16
public static let lg: CGFloat = 24
}
// Corner radius
public enum Radius {
public static let sm: CGFloat = 8
public static let md: CGFloat = 12
public static let capsule: CGFloat = 999
}
}
// App-specific: Theme.swift
enum Theme {
// Inherit from shared theme
static let primary = AppTheme.actionPrimary
static let spacing = AppTheme.Spacing.self
// App-specific expression
static var dialFace: Color {
Color(light: AppTheme.surface, dark: Color(hex: "#1A1A1E"))
}
}
Design Tokens vs Hardcoded Values
| Do | Don't |
|---|---|
Theme.primary | Color.blue |
Theme.Spacing.md | 16 |
Theme.Radius.capsule | 999 |
AppTheme.canvas | Color.white |
Asset Catalog Organization
Assets.xcassets/
Colors/
Primary.colorset/
Surface.colorset/
Images/
AppIcon.appiconset/
Logo.imageset/
Prefer code-defined colors (hex values in theme) for dynamic dark mode support.
iOS 26 Liquid Glass
Glass Background Effects
// Standard glass background
.glassEffect(.regular)
// Glass with interactive (hover/press) feedback
.glassEffect(.regular.interactive())
// Ornament for floating controls
.ornament(visibility: .visible, attachmentAnchor: .scene(.trailing)) {
FloatingControls()
.glassEffect(.regular)
}
Glass Material Hierarchy
// Thick material for modals, sheets
.background(.thickMaterial)
// Regular material for cards, surfaces
.background(.regularMaterial)
// Thin material for subtle overlays
.background(.ultraThinMaterial)
// From theme
AppTheme.glassSurface // .regularMaterial
AppTheme.glassThick // .thickMaterial
AppTheme.glassThin // .ultraThinMaterial
Liquid Glass Best Practices
| Do | Don't |
|---|---|
Use .glassEffect(.regular) for floating UI | Use solid colors for primary surfaces |
| Layer glass at different thicknesses for depth | Overuse glass — it reduces contrast |
Add .hoverEffect(.lift) for interactive elements | Apply glass to text-heavy content |
| Use ornaments for secondary controls | Put glass behind primary action buttons |
Nested Glass & Contrast (macOS/iOS 26)
The #1 cause of "muddy unreadable glass UI" is violating these four rules simultaneously:
Rule 1: Keep glass backing ≤ 8% white opacity for .surface tiers
// ❌ WRONG: White at 20% overwhelms the system tint
.background(shape.fill(Color.white.opacity(0.20)))
// ✅ CORRECT: Let the system Liquid Glass tint dominate
.background(shape.fill(Color.white.opacity(0.08)))
At 20% white, light desktops wash out text; dark desktops create gray sludge. At 8%, the panel refracts the desktop without overwhelming it.
Rule 2: Never put solid Color.opacity() overlays on top of glass
// ❌ WRONG: Three competing opacity layers (desktop → glass → white backing → dark overlay → text)
.background(Color.black.opacity(0.65))
// ✅ CORRECT: True glass.embedded — light refracts through coherent depth
.glassEffect(.regular, in: RoundedRectangle(cornerRadius: 12))
.opacity(0.88)
Solid color overlays on glass create inconsistent contrast that varies with the desktop wallpaper. Use .glassEffect(.regular) at reduced opacity, or .ultraThinMaterial, for embedded rows.
Rule 3: Never nest glassEffect inside glassEffect
// ❌ WRONG: Glass-on-glass causes visual doubling and smearing
GlassPanel(tier: .surface) {
Button("Unlock") { }
.glassEffect(.regular.interactive()) // Nested — fights parent glass
}
// ✅ CORRECT: Use stroke borders or materials for child elements inside glass
.background(
Capsule()
.stroke(accentColor, lineWidth: 1)
)
A .glassEffect(.regular) button inside a .glassEffect(.regular) panel picks up the parent's refraction and creates a low-contrast blob. Use coral hairline strokes, .ultraThinMaterial, or plain text instead.
Rule 4: Glass UIs need explicit hover feedback
// ❌ WRONG: .buttonStyle(.plain) on glass feels dead and unresponsive
Button("Settings…") { }
.buttonStyle(.plain)
// ✅ CORRECT: Add hover states — coral stroke for rows, underline for text
@State private var isHovered = false
// ...
.background(
RoundedRectangle(cornerRadius: 12)
.stroke(isHovered ? accentColor : Color.clear, lineWidth: 1)
)
.onHover { isHovered = $0 }
On glass surfaces, .buttonStyle(.plain) provides zero visual feedback. Every tappable element needs a hover state: coral hairline stroke for rows, underline for text buttons, or scale+lift for prominent actions.
Menubar Dropdown Pattern (macOS 26)
// Surface glass container
GlassPanel(tier: .surface, radius: 18) {
VStack(spacing: 0) {
// Header: sparkle + wordmark
HStack {
Image(systemName: "sparkle")
.foregroundStyle(accentColor)
Text("app.")
.font(.system(size: 16, weight: .ultraLight))
Spacer()
// Trial chip: coral hairline stroke, NOT nested glass
TrialChip()
}
Divider()
// Capture rows: glass.embedded with hover stroke
ForEach(actions) { action in
CaptureRow(action: action)
}
Divider()
// Footer: plain text with hover underline
FooterButton("Settings…")
FooterButton("Quit")
}
.frame(width: 360) // Not 280 — give content room to breathe
}
Key dimensions:
- Width: 360pt (not 280pt — cramped width breaks visual rhythm)
- Backing: ≤ 8% white opacity for surface tier
- Embedded rows:
.glassEffect(.regular)at 0.88 opacity - Buttons inside glass: stroke borders, not nested glass effects
- Hover: coral hairline stroke on rows, underline on text buttons
SwiftUI Patterns
ViewModifiers for Reusable Styles
// Define custom modifiers
struct PrimaryButtonStyle: ViewModifier {
func body(content: Content) -> some View {
content
.font(.headline.weight(.semibold))
.foregroundStyle(.white)
.padding(.horizontal, 24)
.padding(.vertical, 12)
.background(Theme.primary)
.clipShape(Capsule())
}
}
// Extend View for convenience
extension View {
func primaryButtonStyle() -> some View {
modifier(PrimaryButtonStyle())
}
}
// Usage
Button("Start") { }
.primaryButtonStyle()
Container Views for Layout Patterns
// Reusable card container
struct Card<Content: View>: View {
@ViewBuilder let content: Content
var body: some View {
content
.padding(Theme.Spacing.md)
.background(AppTheme.surface)
.cornerRadius(Theme.Radius.md)
.appDepth(.surface)
}
}
// Usage
Card {
VStack(alignment: .leading) {
Text("Title")
Text("Description")
.foregroundStyle(Theme.textSecondary)
}
}
Environment Values for Theme
// Custom environment key
private struct ThemeKey: EnvironmentKey {
static let defaultValue = AppTheme.standard
}
extension EnvironmentValues {
var appTheme: AppTheme {
get { self[ThemeKey.self] }
set { self[ThemeKey.self] = newValue }
}
}
// Usage in view
@Environment(\.appTheme) private var theme
Preview Patterns with PreviewContainer
/// Lightweight container that injects required @Environment objects
@MainActor
struct PreviewContainer<Content: View>: View {
let content: Content
let timerVM: TimerViewModel
let modelContext: ModelContext
init(
timerState: TimerState = .idle,
@ViewBuilder content: () -> Content
) {
self.timerVM = TimerViewModel()
self.timerVM.timerState = timerState
self.content = content()
// Create in-memory model context for previews
self.modelContext = try! ModelContext(
ModelContainer(for: FocusSession.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))
)
}
var body: some View {
content
.environment(timerVM)
.modelContainer(modelContext.container)
}
}
// Preview usage
#Preview("Running State") {
PreviewContainer(timerState: .running) {
TimerView()
}
}
#Preview("Dark Mode") {
PreviewContainer(timerState: .idle) {
TimerView()
}
.preferredColorScheme(.dark)
}
Localization
Infrastructure Setup
Create Localizable.strings early — even for single-language apps. It prevents hardcoded string debt and makes future localization trivial:
App/Resources/
en.lproj/
Localizable.strings
// Localizable.strings
"preview.action.ai" = "Ask this screenshot";
"preview.conversation.inputPlaceholder" = "Ask anything…";
"upgrade.title" = "Unlock Pro";
"lockedFeature.trialButton" = "Try free for %d days";
NSLocalizedString in SwiftUI
// ✅ CORRECT — Use table name for app-specific strings
struct UpgradeSheetCopy {
static var title: String {
NSLocalizedString("upgrade.title", tableName: "Localizable", comment: "Upgrade sheet title")
}
static func trialButton(days: Int) -> String {
String(format: NSLocalizedString("lockedFeature.trialButton", tableName: "Localizable", comment: ""), days)
}
}
// In views
Text(UpgradeSheetCopy.title)
TextField(UpgradeSheetCopy.inputPlaceholder, text: $input)
Migration Path: Hardcoded → Localized
When retrofitting localization into an existing app:
- Extract all user-facing strings to
Localizable.stringswith semantic keys - Replace literals with
NSLocalizedStringcalls - Keep keys namespaced by feature:
feature.element.purpose - Use
String(format: ...)for interpolated values — never concatenate
// ❌ WRONG — concatenation breaks in RTL languages
Text("Try free for " + String(days) + " days")
// ✅ CORRECT — format string handles pluralization and RTL
String(format: NSLocalizedString("lockedFeature.trialButton", comment: ""), days)
Accessibility
Labels and Hints
// Always provide accessibility labels for icons
Image(systemName: "play.fill")
.accessibilityLabel("Start timer")
// Add hints for interactive elements
Button(action: startSession) {
Text("Focus")
}
.accessibilityHint("Double tap to begin a focus session")
// Hide decorative elements
Image(systemName: "sparkles")
.accessibilityHidden(true)
Identifiers for Testing
// Add identifiers for UI testing
Text(timeRemaining)
.accessibilityIdentifier("timeDisplay")
Button(action: pause) {
Image(systemName: "pause.fill")
}
.accessibilityIdentifier("pauseButton")
Dynamic Type Support
// Use scalable font metrics
Text("Title")
.font(.system(.title, design: .rounded))
// Or custom sizes relative to metrics
Text("Body")
.font(.system(size: UIFont.preferredFont(forTextStyle: .body).pointSize))
// Ensure layouts adapt
VStack {
Text("Title")
}
.padding(.horizontal, Theme.Spacing.md)
// Use GeometryReader or @ScaledMetric for size-dependent layouts
VoiceOver Considerations
// Group related elements
VStack {
Text("25:00")
Text("remaining")
}
.accessibilityElement(children: .combine)
.accessibilityLabel("25 minutes remaining")
// Custom actions for complex UI
.accessibilityAction(named: "Add 5 minutes") {
extendSession(by: 300)
}
// Update announcements for state changes
@AccessibilityAction
private func announceCompletion() {
AccessibilityNotification.announce("Focus session complete")
}
Accessibility Checklist
| Element | Required |
|---|---|
| Icon buttons | .accessibilityLabel() |
| Custom controls | .accessibilityLabel() + .accessibilityHint() |
| Test targets | .accessibilityIdentifier() |
| Decorative images | .accessibilityHidden(true) |
| Complex groups | .accessibilityElement(children: .combine) |
| Dynamic text | Use UIFont metrics or .dynamicTypeSize() |
ADHD-Friendly UX Principles
Design for focus, clarity, and reduced cognitive load. Never use "ADHD" in user-facing copy.
Reduce Decision Fatigue
// Do: Smart defaults, minimal choices
struct DurationSelector: View {
let presets = [15, 25, 45, 60] // Curated options
var body: some View {
HStack(spacing: Theme.Spacing.sm) {
ForEach(presets, id: \.self) { minutes in
DurationChip(minutes: minutes)
}
}
}
}
// Don't: Open-ended inputs or overwhelming options
TextField("Enter duration", value: $customMinutes, format: .number)
Clear Visual Hierarchy
// Do: One primary action, clear focal point
VStack(spacing: Theme.Spacing.lg) {
// Hero element (the dial)
TimerDial()
.frame(maxWidth: .infinity)
// Secondary actions in a row
HStack {
SecondaryButton("Adjust") { }
PrimaryButton("Start") { }
}
}
// Don't: Competing primary actions
HStack {
Button("Start") { } // Same weight as...
Button("Settings") { } // ...this
Button("History") { }
}
Immediate Feedback
// Do: Instant visual response
Button(action: { isPressed.toggle() }) {
Image(systemName: isPressed ? "pause.fill" : "play.fill")
}
.buttonStyle(.borderedProminent)
// Do: Haptic feedback for actions
HapticsService.shared.playTap()
// Do: Visual state changes
Circle()
.fill(isActive ? Theme.primary : Theme.surface)
.animation(.easeInOut(duration: 0.2), value: isActive)
Forgiving Interactions
// Do: Easy undo, no destructive confirmations
Button(action: {
withAnimation {
item.delete()
}
}) {
Label("Remove", systemImage: "xmark")
}
// Do: Auto-save, resume where left off
@AppStorage("draftThought") private var draftThought: String = ""
// Do: Gesture forgiveness (larger touch targets)
Button(action: action) {
Image(systemName: "plus")
.frame(width: 44, height: 44) // Minimum 44pt
}
UX Principles Summary
| Principle | Implementation |
|---|---|
| Reduce decision fatigue | Curated presets, smart defaults, progressive disclosure |
| Clear visual hierarchy | One hero element, primary/secondary action distinction |
| Immediate feedback | Haptics, animations, visual state changes |
| Forgiving interactions | Undo support, auto-save, 44pt minimum touch targets |
| Never label as ADHD | Describe benefits: "captures thoughts in under 5 seconds" |
Quick Reference
Common Modifiers
// Depth/shadow
.appDepth(.surface)
.breathingShadow(color: Theme.primary)
// Border
.radiantBorder(color: Theme.primary, intensity: 0.3)
// Glass (iOS 26+)
.glassEffect(.regular)
.glassEffect(.regular.interactive())
// Accessibility
.accessibilityLabel("Description")
.accessibilityHint("Double tap to activate")
.accessibilityIdentifier("uniqueID")
.accessibilityHidden(true)
Theme Values
// Colors
AppTheme.canvas // Background
AppTheme.surface // Cards
AppTheme.actionPrimary // Buttons
AppTheme.textPrimary // Body text
// Spacing
AppTheme.Spacing.xs // 8
AppTheme.Spacing.md // 16
AppTheme.Spacing.lg // 24
// Radius
AppTheme.Radius.sm // 8
AppTheme.Radius.md // 12
AppTheme.Radius.capsule // 999
Preview Template
#Preview("State Name") {
PreviewContainer(timerState: .idle) {
YourView()
}
}
#Preview("Dark Mode") {
PreviewContainer(timerState: .idle) {
YourView()
}
.preferredColorScheme(.dark)
}
See Also
ios-standards— Swift 6 concurrency patternsios26-api-reference— iOS 26 API signaturesios-build— Build validation workflow