Agent Skills: Atomic Design iOS — Expert Decisions

Expert Atomic Design decisions for iOS/tvOS: when component hierarchy adds value vs overkill, atom vs molecule boundary judgment, design token management trade-offs, and component reusability patterns. Use when structuring design systems, deciding component granularity, or organizing component libraries. Trigger keywords: Atomic Design, atoms, molecules, organisms, templates, component library, design system, design tokens, reusability, composition

UncategorizedID: kaakati/rails-enterprise-dev/atomic-design-ios

Install this agent skill to your local

pnpm dlx add-skill https://github.com/Kaakati/rails-enterprise-dev/tree/HEAD/plugins/reactree-ios-dev/skills/atomic-design-ios

Skill Files

Browse the full folder contents for atomic-design-ios.

Download Skill

Loading file tree…

plugins/reactree-ios-dev/skills/atomic-design-ios/SKILL.md

Skill Metadata

Name
atomic-design-ios
Description
"Expert Atomic Design decisions for iOS/tvOS: when component hierarchy adds value vs overkill, atom vs molecule boundary judgment, design token management trade-offs, and component reusability patterns. Use when structuring design systems, deciding component granularity, or organizing component libraries. Trigger keywords: Atomic Design, atoms, molecules, organisms, templates, component library, design system, design tokens, reusability, composition"

Atomic Design iOS — Expert Decisions

Expert decision frameworks for Atomic Design choices in SwiftUI. Claude knows view composition — this skill provides judgment calls for when component hierarchy adds value and how to define boundaries.


Decision Trees

Do You Need Atomic Design?

How large is your design system?
├─ Small (< 10 components)
│  └─ Skip formal hierarchy
│     Simple "Components" folder is fine
│
├─ Medium (10-30 components)
│  └─ Consider Atoms + Molecules
│     Skip Organisms/Templates if not needed
│
└─ Large (30+ components, multiple teams)
   └─ Full Atomic Design hierarchy
      Atoms → Molecules → Organisms → Templates

The trap: Atomic Design for a 5-screen app. The overhead of categorization exceeds the benefit.

Atom vs Molecule Boundary

Does this component combine multiple distinct elements?
├─ NO (single visual element)
│  └─ Atom
│     Button, TextField, Badge, Icon, Label
│
└─ YES (2+ elements that work together)
   └─ Can these elements be used independently?
      ├─ YES → Molecule (SearchBar = Icon + TextField + Button)
      └─ NO → Still Atom (password field with toggle is one unit)

Component Extraction Decision

Will this be used in multiple places?
├─ NO (one-off)
│  └─ Don't extract
│     Inline in parent view
│
├─ YES (2-3 places)
│  └─ Extract as local component
│     Same file or sibling file
│
└─ YES (4+ places or cross-feature)
   └─ Extract to design system
      Full Atom/Molecule treatment

Design Token Scope

What type of value?
├─ Color
│  └─ Is it semantic or brand?
│     ├─ Semantic (error, success) → Color.error, Color.success
│     └─ Brand (primary, accent) → Color.brandPrimary
│
├─ Spacing
│  └─ Use named scale (xs, sm, md, lg, xl)
│     Never magic numbers
│
├─ Typography
│  └─ Use semantic names (body, heading, caption)
│     Map to Font.body, Font.heading
│
└─ Corner radius, shadows
   └─ Named tokens if used consistently
      Radius.card, Shadow.elevated

NEVER Do

Component Design

NEVER create atoms that know about app state:

// ❌ Atom depends on app-level state
struct PrimaryButton: View {
    @EnvironmentObject var authManager: AuthManager

    var body: some View {
        Button(action: action) {
            if authManager.isLoading { ProgressView() }
            else { Text(title) }
        }
    }
}

// ✅ Atom receives all state as parameters
struct PrimaryButton: View {
    let title: String
    let action: () -> Void
    var isLoading: Bool = false

    var body: some View {
        Button(action: action) {
            if isLoading { ProgressView() }
            else { Text(title) }
        }
    }
}

NEVER hardcode values in components:

// ❌ Magic numbers everywhere
struct Card: View {
    var body: some View {
        content
            .padding(16)  // Magic number
            .background(Color(hex: "#FFFFFF"))  // Hardcoded
            .cornerRadius(12)  // Magic number
    }
}

// ✅ Use design tokens
struct Card: View {
    var body: some View {
        content
            .padding(Spacing.md)
            .background(Color.surface)
            .cornerRadius(Radius.card)
    }
}

NEVER create components with too many parameters:

// ❌ Too many parameters — hard to use
struct ComplexButton: View {
    let title: String
    let subtitle: String?
    let icon: String?
    let iconPosition: IconPosition
    let size: Size
    let style: Style
    let isLoading: Bool
    let isEnabled: Bool
    let hasBorder: Bool
    let cornerRadius: CGFloat
    // ... 10 more parameters
}

// ✅ Split into focused variants
struct PrimaryButton: View { ... }
struct SecondaryButton: View { ... }
struct IconButton: View { ... }
struct LoadingButton: View { ... }

Hierarchy Mistakes

NEVER skip levels in composition:

// ❌ Template directly uses atoms (no molecules/organisms)
struct ProductListTemplate: View {
    var body: some View {
        ForEach(products) { product in
            // Building organism inline from atoms
            HStack {
                AsyncImage(url: product.imageURL)
                VStack {
                    Text(product.name).font(.headline)
                    Text("$\(product.price)").foregroundColor(.blue)
                }
                Button("Add") { }
            }
        }
    }
}

// ✅ Template uses organisms
struct ProductListTemplate: View {
    var body: some View {
        ForEach(products) { product in
            ProductCard(product: product, onAddToCart: { })
        }
    }
}

NEVER put business logic in design system components:

// ❌ Organism fetches data
struct UserCard: View {
    @StateObject private var viewModel = UserViewModel()

    var body: some View {
        Card {
            // Uses viewModel.user
        }
        .onAppear { viewModel.load() }
    }
}

// ✅ Organism is purely presentational
struct UserCard: View {
    let user: User
    let onTap: () -> Void

    var body: some View {
        Card {
            // Uses passed-in user
        }
    }
}

Design Token Mistakes

NEVER use platform colors directly:

// ❌ Hardcoded system colors
.foregroundColor(.blue)
.background(Color(.systemGray6))

// ✅ Semantic tokens that can be themed
.foregroundColor(Color.interactive)
.background(Color.surfaceSecondary)

NEVER duplicate token definitions:

// ❌ Same value defined in multiple places
struct Card { let cornerRadius: CGFloat = 12 }
struct Button { let cornerRadius: CGFloat = 12 }
struct TextField { let cornerRadius: CGFloat = 12 }

// ✅ Single source of truth
enum Radius {
    static let sm: CGFloat = 4
    static let md: CGFloat = 8
    static let lg: CGFloat = 12
}

struct Card { ... .cornerRadius(Radius.lg) }

Essential Patterns

Token System Structure

// Spacing tokens
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 = 32
}

// Color tokens (support dark mode)
extension Color {
    // Semantic
    static let textPrimary = Color("TextPrimary")
    static let textSecondary = Color("TextSecondary")
    static let surface = Color("Surface")
    static let surfaceSecondary = Color("SurfaceSecondary")

    // Brand
    static let brandPrimary = Color("BrandPrimary")
    static let brandAccent = Color("BrandAccent")

    // Feedback
    static let success = Color("Success")
    static let warning = Color("Warning")
    static let error = Color("Error")
}

// Typography tokens
extension Font {
    static let displayLarge = Font.system(size: 34, weight: .bold)
    static let heading1 = Font.system(size: 28, weight: .bold)
    static let heading2 = Font.system(size: 22, weight: .semibold)
    static let bodyLarge = Font.system(size: 17)
    static let bodyRegular = Font.system(size: 15)
    static let caption = Font.system(size: 13)
}

Composable Atom Pattern

// Atom with sensible defaults and overrides
struct PrimaryButton: View {
    let title: String
    let action: () -> Void
    var isLoading: Bool = false
    var isEnabled: Bool = true
    var size: Size = .regular

    enum Size {
        case small, regular, large

        var padding: EdgeInsets {
            switch self {
            case .small: return EdgeInsets(horizontal: Spacing.sm, vertical: Spacing.xs)
            case .regular: return EdgeInsets(horizontal: Spacing.md, vertical: Spacing.sm)
            case .large: return EdgeInsets(horizontal: Spacing.lg, vertical: Spacing.md)
            }
        }

        var font: Font {
            switch self {
            case .small: return .caption
            case .regular: return .bodyRegular
            case .large: return .heading2
            }
        }
    }

    var body: some View {
        Button(action: action) {
            Group {
                if isLoading {
                    ProgressView()
                } else {
                    Text(title).font(size.font)
                }
            }
            .frame(maxWidth: .infinity)
            .padding(size.padding)
        }
        .background(isEnabled ? Color.brandPrimary : Color.textSecondary)
        .foregroundColor(.white)
        .cornerRadius(Radius.md)
        .disabled(!isEnabled || isLoading)
    }
}

Molecule with Slot Pattern

// Generic molecule with customizable slots
struct Card<Content: View, Footer: View>: View {
    let content: Content
    let footer: Footer?

    init(
        @ViewBuilder content: () -> Content,
        @ViewBuilder footer: () -> Footer
    ) {
        self.content = content()
        self.footer = footer()
    }

    var body: some View {
        VStack(alignment: .leading, spacing: Spacing.md) {
            content

            if let footer = footer {
                Divider()
                footer
            }
        }
        .padding(Spacing.md)
        .background(Color.surface)
        .cornerRadius(Radius.lg)
        .shadow(color: .black.opacity(0.1), radius: 4, y: 2)
    }
}

// Convenience initializer without footer
extension Card where Footer == EmptyView {
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
        self.footer = nil
    }
}

Quick Reference

When to Extract Components

| Scenario | Action | |----------|--------| | Used once | Keep inline | | Used 2-3 times in same feature | Local extraction | | Used across features | Design system component | | Complex but single-use | Extract for readability only |

Component Classification

| Level | Examples | Knows About | |-------|----------|-------------| | Atom | Button, TextField, Icon, Badge | Nothing external | | Molecule | SearchBar, FormInput, Card | Atoms only | | Organism | NavigationBar, ProductCard, UserList | Atoms + Molecules | | Template | ListPageLayout, FormLayout | Organisms |

Design Token Categories

| Category | Token Examples | |----------|----------------| | Spacing | xs, sm, md, lg, xl | | Color | textPrimary, surface, brandPrimary, error | | Typography | displayLarge, heading1, body, caption | | Radius | sm, md, lg, full | | Shadow | subtle, elevated, prominent |

Red Flags

| Smell | Problem | Fix | |-------|---------|-----| | Atom uses @EnvironmentObject | Knows too much | Pass state as params | | 10+ parameters on component | Too flexible | Split into variants | | Magic numbers in components | Not themeable | Use tokens | | Template builds from atoms | Skipping levels | Use molecules/organisms | | Different corner radius per component | Inconsistency | Token system | | Component fetches data | Wrong layer | Presentational only |