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