agentsclimarketplace

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.

Install
npx -y skills add markdavidgan/apple-dev-skills --skill apple-design

Assembled 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

DoDon't
Theme.primaryColor.blue
Theme.Spacing.md16
Theme.Radius.capsule999
AppTheme.canvasColor.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

DoDon't
Use .glassEffect(.regular) for floating UIUse solid colors for primary surfaces
Layer glass at different thicknesses for depthOveruse glass — it reduces contrast
Add .hoverEffect(.lift) for interactive elementsApply glass to text-heavy content
Use ornaments for secondary controlsPut 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:

  1. Extract all user-facing strings to Localizable.strings with semantic keys
  2. Replace literals with NSLocalizedString calls
  3. Keep keys namespaced by feature: feature.element.purpose
  4. 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

ElementRequired
Icon buttons.accessibilityLabel()
Custom controls.accessibilityLabel() + .accessibilityHint()
Test targets.accessibilityIdentifier()
Decorative images.accessibilityHidden(true)
Complex groups.accessibilityElement(children: .combine)
Dynamic textUse 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

PrincipleImplementation
Reduce decision fatigueCurated presets, smart defaults, progressive disclosure
Clear visual hierarchyOne hero element, primary/secondary action distinction
Immediate feedbackHaptics, animations, visual state changes
Forgiving interactionsUndo support, auto-save, 44pt minimum touch targets
Never label as ADHDDescribe 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 patterns
  • ios26-api-reference — iOS 26 API signatures
  • ios-build — Build validation workflow

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.