npx skills add ...
npx skills add jamesrochabrun/skills --skill apple-hig-designer
Design iOS apps following Apple's Human Interface Guidelines. Generate native components, validate designs, and ensure accessibility compliance for iPhone, iPad, and Apple Watch.
npx skills add jamesrochabrun/skills --skill apple-hig-designer
Design beautiful, native iOS apps following Apple's Human Interface Guidelines (HIG). Create accessible, intuitive interfaces with native components, proper typography, semantic colors, and Apple's design principles.
Helps you design and build iOS apps that feel native and follow Apple's guidelines:
Make content clear and focused.
Text is legible at every size, icons are precise and lucid, adornments are subtle and appropriate, and a focus on functionality drives the design.
UI helps people understand and interact with content, but never competes with it.
The interface defers to content, using a light visual treatment that keeps focus on the content and gives the content room to breathe.
Visual layers and realistic motion convey hierarchy and help people understand relationships.
Distinct visual layers and realistic motion impart vitality and facilitate understanding. Touch and discoverability heighten delight and enable access to functionality without losing context.
Top bar for navigation and actions.
Guidelines:
Bottom navigation for top-level destinations.
Guidelines:
Scrollable list of items.
List Styles:
.plain - Edge-to-edge rows.insetGrouped - Rounded, inset sections (iOS default).sidebar - For navigation sidebarsPresent content modally.
Sheet Detents:
.medium - Half screen.large - Full screenPrimary action control.
Button Hierarchy:
Guidelines:
Text input control.
Text Content Types:
.username - Username field.password - Password field.emailAddress - Email field.telephoneNumber - Phone number.creditCardNumber - Credit cardBoolean control (switch).
Guidelines:
Selection control.
Picker Styles:
.menu - Dropdown menu (default).segmented - Segmented control (2-5 options).wheel - Scrollable wheel.inline - Inline list (in forms)Apple's system font designed for optimal legibility.
Do:
Don't:
Colors that automatically adapt to light/dark mode.
WCAG AA Compliance:
Custom colors:
All spacing should be multiples of 8.
Respect device safe areas.
Minimum interactive size: 44x44 points.
Screen reader for blind and low-vision users.
Support user's preferred text size.
Design for color-blind users.
Respect user's motion preferences.
Support high contrast mode.
Support both light and dark appearances.
Do:
Don't:
Apple's icon system (3000+ symbols).
Do:
Don't:
"Design is not just what it looks like and feels like. Design is how it works." - Steve Jobs
// ✅ Content-focused
VStack(alignment: .leading, spacing: 8) {
Text("Article Title")
.font(.headline)
Text("Article content goes here...")
.font(.body)
.foregroundColor(.secondary)
}
.padding()
// ❌ Distracting UI
VStack(spacing: 8) {
Text("Article Title")
.font(.headline)
.foregroundColor(.white)
.background(.blue)
.border(.red, width: 3)
}// ✅ Clear depth hierarchy
ZStack {
Color(.systemBackground)
VStack {
// Card with elevation
CardView()
.shadow(radius: 8)
}
}
// Using blur for depth
Text("Content")
.background(.ultraThinMaterial)NavigationStack {
List {
Text("Item 1")
Text("Item 2")
}
.navigationTitle("Title")
.navigationBarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add") {
// Action
}
}
}
}TabView {
HomeView()
.tabItem {
Label("Home", systemImage: "house")
}
SearchView()
.tabItem {
Label("Search", systemImage: "magnifyingglass")
}
ProfileView()
.tabItem {
Label("Profile", systemImage: "person")
}
}List {
Section("Today") {
ForEach(items) { item in
NavigationLink {
DetailView(item: item)
} label: {
HStack {
Image(systemName: item.icon)
.foregroundColor(.accentColor)
Text(item.title)
}
}
}
}
}
.listStyle(.insetGrouped)struct ContentView: View {
@State private var showSheet = false
var body: some View {
Button("Show Details") {
showSheet = true
}
.sheet(isPresented: $showSheet) {
DetailView()
.presentationDetents([.medium, .large])
}
}
}// Filled button (primary action)
Button("Continue") {
// Action
}
.buttonStyle(.borderedProminent)
// Bordered button (secondary action)
Button("Cancel") {
// Action
}
.buttonStyle(.bordered)
// Plain button (tertiary action)
Button("Learn More") {
// Action
}
.buttonStyle(.plain)@State private var username = ""
@State private var password = ""
VStack(alignment: .leading, spacing: 16) {
// Standard text field
TextField("Username", text: $username)
.textFieldStyle(.roundedBorder)
.textContentType(.username)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
// Secure field
SecureField("Password", text: $password)
.textFieldStyle(.roundedBorder)
.textContentType(.password)
}@State private var isEnabled = false
Toggle("Enable notifications", isOn: $isEnabled)
.toggleStyle(.switch)@State private var selectedSize = "Medium"
let sizes = ["Small", "Medium", "Large"]
// Menu style
Picker("Size", selection: $selectedSize) {
ForEach(sizes, id: \.self) { size in
Text(size).tag(size)
}
}
.pickerStyle(.menu)
// Segmented style (for 2-5 options)
Picker("Size", selection: $selectedSize) {
ForEach(sizes, id: \.self) { size in
Text(size).tag(size)
}
}
.pickerStyle(.segmented)struct CardView: View {
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Title")
.font(.headline)
Text("Description goes here with some details about the content.")
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(2)
Spacer()
Button("Action") {
// Action
}
.buttonStyle(.borderedProminent)
}
.padding()
.frame(width: 300, height: 200)
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 4)
}
}// Dynamic Type text styles
Text("Large Title").font(.largeTitle) // 34pt
Text("Title").font(.title) // 28pt
Text("Title 2").font(.title2) // 22pt
Text("Title 3").font(.title3) // 20pt
Text("Headline").font(.headline) // 17pt semibold
Text("Body").font(.body) // 17pt regular
Text("Callout").font(.callout) // 16pt
Text("Subheadline").font(.subheadline) // 15pt
Text("Footnote").font(.footnote) // 13pt
Text("Caption").font(.caption) // 12pt
Text("Caption 2").font(.caption2) // 11pt// Custom font that scales with Dynamic Type
Text("Custom Text")
.font(.custom("YourFont-Regular", size: 17, relativeTo: .body))Text("Light").fontWeight(.light)
Text("Regular").fontWeight(.regular)
Text("Medium").fontWeight(.medium)
Text("Semibold").fontWeight(.semibold)
Text("Bold").fontWeight(.bold)
Text("Heavy").fontWeight(.heavy)// UI Element Colors
Color(.label) // Primary text
Color(.secondaryLabel) // Secondary text
Color(.tertiaryLabel) // Tertiary text
Color(.quaternaryLabel) // Watermark text
Color(.systemBackground) // Primary background
Color(.secondarySystemBackground) // Secondary background
Color(.tertiarySystemBackground) // Tertiary background
Color(.systemFill) // Fill colors
Color(.secondarySystemFill)
Color(.tertiarySystemFill)
Color(.quaternarySystemFill)
Color(.separator) // Separator lines
Color(.opaqueSeparator) // Non-transparent separator// Standard system colors (adapt to dark mode)
Color(.systemRed)
Color(.systemOrange)
Color(.systemYellow)
Color(.systemGreen)
Color(.systemMint)
Color(.systemTeal)
Color(.systemCyan)
Color(.systemBlue)
Color(.systemIndigo)
Color(.systemPurple)
Color(.systemPink)
Color(.systemBrown)
Color(.systemGray)// Define adaptive color
extension Color {
static let customBackground = Color("CustomBackground")
}
// In Assets.xcassets, create color set with:
// - Any Appearance: #FFFFFF
// - Dark Appearance: #000000// Spacing values
.padding(8) // 8pt
.padding(16) // 16pt (standard)
.padding(24) // 24pt
.padding(32) // 32pt
.padding(40) // 40pt
.padding(48) // 48pt
// Edge-specific padding
.padding(.horizontal, 16)
.padding(.vertical, 24)
.padding(.top, 16)
.padding(.bottom, 16)// Content within safe area (default)
VStack {
Text("Content")
}
// Extend beyond safe area
VStack {
Color.blue
}
.ignoresSafeArea()
// Extend top only
VStack {
Color.blue
}
.ignoresSafeArea(edges: .top)Button("Tap") {
// Action
}
.frame(minWidth: 44, minHeight: 44)// Component spacing
VStack(spacing: 8) { // Tight spacing
Text("Line 1")
Text("Line 2")
}
VStack(spacing: 16) { // Standard spacing
Text("Section 1")
Text("Section 2")
}
VStack(spacing: 24) { // Loose spacing
SectionView()
SectionView()
}// Accessible label
Image(systemName: "heart.fill")
.accessibilityLabel("Favorite")
// Accessible value
Slider(value: $volume)
.accessibilityLabel("Volume")
.accessibilityValue("\(Int(volume * 100))%")
// Accessible hint
Button("Share") {
share()
}
.accessibilityHint("Shares this item with others")
// Group elements
HStack {
Image(systemName: "person")
Text("John Doe")
}
.accessibilityElement(children: .combine)
// Hidden from VoiceOver
Image("decorative")
.accessibilityHidden(true)// Automatically supported with system fonts
Text("This text scales")
.font(.body)
// Limit scaling (if necessary)
Text("This text has limits")
.font(.body)
.dynamicTypeSize(...DynamicTypeSize.xxxLarge)
// Custom font with Dynamic Type
Text("Custom font")
.font(.custom("YourFont", size: 17, relativeTo: .body))// Don't rely on color alone
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text("Success")
}
// Not just color
Circle()
.fill(.green)
// ❌ Color only
// Better with shape/icon
HStack {
Image(systemName: "checkmark.circle.fill")
Circle().fill(.green)
}
// ✅ Color + shape@Environment(\.accessibilityReduceMotion) var reduceMotion
var animation: Animation {
reduceMotion ? .none : .spring()
}
Button("Animate") {
withAnimation(animation) {
// Animate
}
}@Environment(\.colorSchemeContrast) var contrast
var textColor: Color {
contrast == .increased ? .primary : .secondary
}
Text("Content")
.foregroundColor(textColor)// Use semantic colors (automatic)
Color(.label) // Adapts automatically
Color(.systemBackground) // Adapts automatically// Preview both modes
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.preferredColorScheme(.light)
ContentView()
.preferredColorScheme(.dark)
}
}// Basic symbol
Image(systemName: "heart")
// Colored symbol
Image(systemName: "heart.fill")
.foregroundColor(.red)
// Sized symbol
Image(systemName: "heart")
.imageScale(.large)
// Font-based sizing
Image(systemName: "heart")
.font(.title)
// Multicolor symbols
Image(systemName: "person.crop.circle.fill.badge.checkmark")
.symbolRenderingMode(.multicolor)
// Hierarchical rendering
Image(systemName: "heart.fill")
.symbolRenderingMode(.hierarchical)
.foregroundColor(.red)iOS:
- 1024x1024 (App Store)
- 180x180 (iPhone @3x)
- 120x120 (iPhone @2x)
- 167x167 (iPad Pro)
- 152x152 (iPad @2x)
watchOS:
- 1024x1024 (App Store)
- 196x196 (49mm)
- 216x216 (45mm)// Spring animation (natural, bouncy)
withAnimation(.spring()) {
offset = 100
}
// Linear animation
withAnimation(.linear(duration: 0.3)) {
opacity = 0
}
// Ease in/out
withAnimation(.easeInOut(duration: 0.3)) {
scale = 1.2
}@State private var offset = CGSize.zero
var body: some View {
Circle()
.offset(offset)
.gesture(
DragGesture()
.onChanged { value in
offset = value.translation
}
.onEnded { _ in
withAnimation(.spring()) {
offset = .zero
}
}
)
}struct LoadingView: View {
var body: some View {
VStack {
ProgressView()
.scaleEffect(1.5)
Text("Loading...")
.font(.caption)
.foregroundColor(.secondary)
.padding(.top)
}
}
}struct ErrorView: View {
let message: String
let retry: () -> Void
var body: some View {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 48))
.foregroundColor(.orange)
Text("Something went wrong")
.font(.headline)
Text(message)
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
Button("Try Again") {
retry()
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}struct EmptyStateView: View {
var body: some View {
VStack(spacing: 16) {
Image(systemName: "tray")
.font(.system(size: 64))
.foregroundColor(.secondary)
Text("No Items")
.font(.title2)
Text("Your items will appear here")
.font(.subheadline)
.foregroundColor(.secondary)
Button("Add Item") {
// Action
}
.buttonStyle(.borderedProminent)
}
}
}