AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Apple Design

skill-markdavidgan-apple-dev-skills-apple-design · by markdavidgan

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 cons…

No reviews yet
0 installs
38 views
0.0% view→install

Install

$ agentstack add skill-markdavidgan-apple-dev-skills-apple-design

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-markdavidgan-apple-dev-skills-apple-design)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Apple Design? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About


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: 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: 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

| 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

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [markdavidgan](https://github.com/markdavidgan)
- **Source:** [markdavidgan/apple-dev-skills](https://github.com/markdavidgan/apple-dev-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.