commit 37931844a6a9ff803beb10ee1d0f7d58c03da167 Author: Zhongwei Li Date: Sat Nov 29 18:49:07 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..a78f173 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "apple-hig-designer", + "description": "Design iOS apps following Apple's Human Interface Guidelines. Generate native components, validate designs, and ensure accessibility compliance for iPhone, iPad, and Apple Watch.", + "version": "0.0.0-2025.11.28", + "author": { + "name": "James Rochabrun", + "email": "jamesrochabrun@gmail.com" + }, + "skills": [ + "./skills/apple-hig-designer" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5928f03 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# 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. diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..fb3ab85 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,76 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:jamesrochabrun/skills:apple-hig-designer", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "4253758c076edb94de32c8686f10a8d34c64fe9f", + "treeHash": "f2d042a8ec0e8aba0a5d311d834b29c270da0557a4f73aaa1715e662a4344f1d", + "generatedAt": "2025-11-28T10:17:44.649406Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "apple-hig-designer", + "description": "Design iOS apps following Apple's Human Interface Guidelines. Generate native components, validate designs, and ensure accessibility compliance for iPhone, iPad, and Apple Watch." + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "f9bd30b6ddf127f57595c629cab1a326c3f1bae2d95158b6a1aa6f7008784b51" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "6c6f18c3182c6f57705641c68db1b4b75314c94fd5ce15d5bc961bdac715df62" + }, + { + "path": "skills/apple-hig-designer/SKILL.md", + "sha256": "f26ba494d79da53a8c7189c2cb887412473aa2ac0ded98b93d8d10c68d88daea" + }, + { + "path": "skills/apple-hig-designer/references/colors.md", + "sha256": "688da20a8105536ac093bb111a39385b51357f0dd086916576a94b2a710ffb72" + }, + { + "path": "skills/apple-hig-designer/references/layout_spacing.md", + "sha256": "d2a6e2211b31ee5da00c16443000e953a6b07989678cb6d2039f17021217c581" + }, + { + "path": "skills/apple-hig-designer/references/components.md", + "sha256": "c242bfaa877a8a0cb000ef0729e53fe2e984f91bfcc17fb751e82abf3c8244e8" + }, + { + "path": "skills/apple-hig-designer/references/accessibility.md", + "sha256": "483f8a27794bb86afa085997af4e1cec0d674f98929c0008a99dffdc11e863e8" + }, + { + "path": "skills/apple-hig-designer/references/typography.md", + "sha256": "a37f5f3f5bcafdbc6a665109edf24c75c095efb2c1ab586497a34c171ed26381" + }, + { + "path": "skills/apple-hig-designer/scripts/validate_design.sh", + "sha256": "a3aa1cea0a5026819178a04fa823109bc4230e2478ee4e13707f291304b8d133" + }, + { + "path": "skills/apple-hig-designer/scripts/generate_ios_component.sh", + "sha256": "1785850e802c3bab77d7a8ecc1c838cb1fb78f5ef7ae96d82943d5d3ddfb06a3" + }, + { + "path": "skills/apple-hig-designer/scripts/audit_accessibility.sh", + "sha256": "27c227e0464a78cc7f2f99e08c9b2e9e4b728ed4d90547eec6b7276e9755f084" + } + ], + "dirSha256": "f2d042a8ec0e8aba0a5d311d834b29c270da0557a4f73aaa1715e662a4344f1d" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/apple-hig-designer/SKILL.md b/skills/apple-hig-designer/SKILL.md new file mode 100644 index 0000000..b919817 --- /dev/null +++ b/skills/apple-hig-designer/SKILL.md @@ -0,0 +1,946 @@ +--- +name: apple-hig-designer +description: Design iOS apps following Apple's Human Interface Guidelines. Generate native components, validate designs, and ensure accessibility compliance for iPhone, iPad, and Apple Watch. +--- + +# 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. + +## What This Skill Does + +Helps you design and build iOS apps that feel native and follow Apple's guidelines: +- **Generate iOS Components** - Create SwiftUI and UIKit components +- **Validate Designs** - Check compliance with Apple HIG +- **Ensure Accessibility** - VoiceOver, Dynamic Type, color contrast +- **Apply Design Principles** - Clarity, Deference, Depth +- **Use Semantic Colors** - Automatic dark mode support +- **Implement Typography** - San Francisco font system +- **Follow Spacing** - 8pt grid system and safe areas + +## Apple's Design Principles + +### 1. Clarity + +**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. + +```swift +// ✅ Clear, focused content +Text("Welcome back, Sarah") + .font(.title) + .foregroundColor(.primary) + +// ❌ Unclear, cluttered +Text("Welcome back, Sarah!!!") + .font(.title) + .foregroundColor(.red) + .background(.yellow) + .overlay(Image(systemName: "star.fill")) +``` + +### 2. Deference + +**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. + +```swift +// ✅ 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) +} +``` + +### 3. Depth + +**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. + +```swift +// ✅ Clear depth hierarchy +ZStack { + Color(.systemBackground) + + VStack { + // Card with elevation + CardView() + .shadow(radius: 8) + } +} + +// Using blur for depth +Text("Content") + .background(.ultraThinMaterial) +``` + +## iOS UI Components + +### Navigation Patterns + +#### 1. Navigation Bar + +**Top bar for navigation and actions.** + +```swift +NavigationStack { + List { + Text("Item 1") + Text("Item 2") + } + .navigationTitle("Title") + .navigationBarTitleDisplayMode(.large) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Add") { + // Action + } + } + } +} +``` + +**Guidelines:** +- Use large titles for top-level views +- Use inline titles for detail views +- Keep actions relevant to current context +- Maximum 2-3 toolbar items + +#### 2. Tab Bar + +**Bottom navigation for top-level destinations.** + +```swift +TabView { + HomeView() + .tabItem { + Label("Home", systemImage: "house") + } + + SearchView() + .tabItem { + Label("Search", systemImage: "magnifyingglass") + } + + ProfileView() + .tabItem { + Label("Profile", systemImage: "person") + } +} +``` + +**Guidelines:** +- 3-5 tabs maximum +- Use SF Symbols for icons +- Labels should be concise (one word) +- Never hide or disable tabs +- Don't use tab bar with toolbar in same view + +#### 3. List + +**Scrollable list of items.** + +```swift +List { + Section("Today") { + ForEach(items) { item in + NavigationLink { + DetailView(item: item) + } label: { + HStack { + Image(systemName: item.icon) + .foregroundColor(.accentColor) + Text(item.title) + } + } + } + } +} +.listStyle(.insetGrouped) +``` + +**List Styles:** +- `.plain` - Edge-to-edge rows +- `.insetGrouped` - Rounded, inset sections (iOS default) +- `.sidebar` - For navigation sidebars + +#### 4. Sheet (Modal) + +**Present content modally.** + +```swift +struct ContentView: View { + @State private var showSheet = false + + var body: some View { + Button("Show Details") { + showSheet = true + } + .sheet(isPresented: $showSheet) { + DetailView() + .presentationDetents([.medium, .large]) + } + } +} +``` + +**Sheet Detents:** +- `.medium` - Half screen +- `.large` - Full screen +- Custom heights available + +### Form Controls + +#### 1. Button + +**Primary action control.** + +```swift +// 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) +``` + +**Button Hierarchy:** +1. **Prominent** - Primary action (one per screen) +2. **Bordered** - Secondary actions +3. **Plain** - Tertiary actions, links + +**Guidelines:** +- Minimum tap target: 44x44 points +- Use verbs for button labels +- Make destructive actions require confirmation + +#### 2. TextField + +**Text input control.** + +```swift +@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) +} +``` + +**Text Content Types:** +- `.username` - Username field +- `.password` - Password field +- `.emailAddress` - Email field +- `.telephoneNumber` - Phone number +- `.creditCardNumber` - Credit card + +#### 3. Toggle + +**Boolean control (switch).** + +```swift +@State private var isEnabled = false + +Toggle("Enable notifications", isOn: $isEnabled) + .toggleStyle(.switch) +``` + +**Guidelines:** +- Label describes what the toggle controls +- Effect should be immediate +- Use for binary choices only + +#### 4. Picker + +**Selection control.** + +```swift +@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) +``` + +**Picker Styles:** +- `.menu` - Dropdown menu (default) +- `.segmented` - Segmented control (2-5 options) +- `.wheel` - Scrollable wheel +- `.inline` - Inline list (in forms) + +### Cards and Containers + +#### Card View + +```swift +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) + } +} +``` + +## Typography + +### San Francisco Font System + +Apple's system font designed for optimal legibility. + +```swift +// 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 Fonts with Dynamic Type + +```swift +// Custom font that scales with Dynamic Type +Text("Custom Text") + .font(.custom("YourFont-Regular", size: 17, relativeTo: .body)) +``` + +### Font Weights + +```swift +Text("Light").fontWeight(.light) +Text("Regular").fontWeight(.regular) +Text("Medium").fontWeight(.medium) +Text("Semibold").fontWeight(.semibold) +Text("Bold").fontWeight(.bold) +Text("Heavy").fontWeight(.heavy) +``` + +### Typography Guidelines + +**Do:** +- ✅ Use system font (San Francisco) for consistency +- ✅ Support Dynamic Type for accessibility +- ✅ Use semantic text styles (.headline, .body, etc.) +- ✅ Minimum body text: 17pt +- ✅ Line spacing: 120-145% of font size + +**Don't:** +- ❌ Use too many font sizes (stick to system styles) +- ❌ Make text smaller than 11pt +- ❌ Use all caps for long text +- ❌ Disable Dynamic Type + +## Colors + +### Semantic Colors + +**Colors that automatically adapt to light/dark mode.** + +```swift +// 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 +``` + +### System Colors + +```swift +// 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) +``` + +### Custom Colors with Dark Mode + +```swift +// Define adaptive color +extension Color { + static let customBackground = Color("CustomBackground") +} + +// In Assets.xcassets, create color set with: +// - Any Appearance: #FFFFFF +// - Dark Appearance: #000000 +``` + +### Color Contrast Guidelines + +**WCAG AA Compliance:** +- Normal text: 4.5:1 contrast ratio minimum +- Large text (24pt+): 3:1 contrast ratio minimum +- UI components: 3:1 contrast ratio + +**Custom colors:** +- Test with Increase Contrast enabled +- Aim for 7:1 for critical text +- Provide sufficient contrast in both modes + +## Spacing and Layout + +### 8-Point Grid System + +**All spacing should be multiples of 8.** + +```swift +// 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) +``` + +### Safe Areas + +**Respect device safe areas.** + +```swift +// 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) +``` + +### Touch Targets + +**Minimum interactive size: 44x44 points.** + +```swift +Button("Tap") { + // Action +} +.frame(minWidth: 44, minHeight: 44) +``` + +### Spacing Guidelines + +```swift +// 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() +} +``` + +## Accessibility + +### VoiceOver Support + +**Screen reader for blind and low-vision users.** + +```swift +// 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) +``` + +### Dynamic Type + +**Support user's preferred text size.** + +```swift +// 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)) +``` + +### Color Blindness + +**Design for color-blind users.** + +```swift +// 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 +``` + +### Reduce Motion + +**Respect user's motion preferences.** + +```swift +@Environment(\.accessibilityReduceMotion) var reduceMotion + +var animation: Animation { + reduceMotion ? .none : .spring() +} + +Button("Animate") { + withAnimation(animation) { + // Animate + } +} +``` + +### Increase Contrast + +**Support high contrast mode.** + +```swift +@Environment(\.colorSchemeContrast) var contrast + +var textColor: Color { + contrast == .increased ? .primary : .secondary +} + +Text("Content") + .foregroundColor(textColor) +``` + +## Dark Mode + +**Support both light and dark appearances.** + +### Automatic Support + +```swift +// Use semantic colors (automatic) +Color(.label) // Adapts automatically +Color(.systemBackground) // Adapts automatically +``` + +### Testing Dark Mode + +```swift +// Preview both modes +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + .preferredColorScheme(.light) + + ContentView() + .preferredColorScheme(.dark) + } +} +``` + +### Dark Mode Guidelines + +**Do:** +- ✅ Use semantic colors +- ✅ Test with Increase Contrast +- ✅ Test with Reduce Transparency +- ✅ Ensure sufficient contrast in both modes + +**Don't:** +- ❌ Use pure black (#000000) - use systemBackground +- ❌ Invert colors automatically +- ❌ Assume user preference + +## SF Symbols + +**Apple's icon system (3000+ symbols).** + +```swift +// 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) +``` + +### SF Symbols Guidelines + +- Use system symbols when available +- Maintain visual weight consistency +- Use multicolor for semantic meaning +- Size appropriately for context + +## App Icons + +### Icon Sizes + +``` +iOS: +- 1024x1024 (App Store) +- 180x180 (iPhone @3x) +- 120x120 (iPhone @2x) +- 167x167 (iPad Pro) +- 152x152 (iPad @2x) + +watchOS: +- 1024x1024 (App Store) +- 196x196 (49mm) +- 216x216 (45mm) +``` + +### Icon Design Guidelines + +**Do:** +- ✅ Use simple, recognizable shapes +- ✅ Fill entire icon space +- ✅ Test on device (not just mockups) +- ✅ Use consistent visual style + +**Don't:** +- ❌ Include text (very small) +- ❌ Use photos +- ❌ Replicate Apple hardware +- ❌ Use translucency + +## Animation and Motion + +### Standard Animations + +```swift +// 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 +} +``` + +### Gesture-Driven + +```swift +@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 + } + } + ) +} +``` + +### Motion Guidelines + +- Keep animations under 0.3 seconds +- Use spring animations for interactive elements +- Respect Reduce Motion setting +- Provide visual feedback for all interactions + +## Best Practices + +### Navigation + +- **Hierarchical** - Use NavigationStack for drilldown +- **Flat** - Use TabView for peer destinations +- **Content-Driven** - Use for media apps + +### Feedback + +- **Visual** - Highlight on tap +- **Haptic** - Use UIImpactFeedbackGenerator +- **Audio** - Use system sounds sparingly + +### Loading States + +```swift +struct LoadingView: View { + var body: some View { + VStack { + ProgressView() + .scaleEffect(1.5) + Text("Loading...") + .font(.caption) + .foregroundColor(.secondary) + .padding(.top) + } + } +} +``` + +### Error States + +```swift +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() + } +} +``` + +### Empty States + +```swift +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) + } + } +} +``` + +## Platform Considerations + +### iPhone + +- Design for various sizes (SE, Pro, Pro Max) +- Support portrait and landscape +- Use safe areas for notch/Dynamic Island +- Consider one-handed use + +### iPad + +- Support multitasking (Split View, Slide Over) +- Use sidebars for navigation +- Adapt to larger screen (don't just scale) +- Consider keyboard shortcuts +- Support external displays + +### Apple Watch + +- Glanceable information +- Large touch targets (>44pt) +- Minimal interaction required +- Use Digital Crown for scrolling +- Support Always-On display + +## Resources + +- [Apple HIG Official](https://developer.apple.com/design/human-interface-guidelines/) +- [SF Symbols App](https://developer.apple.com/sf-symbols/) +- [WWDC Videos](https://developer.apple.com/videos/) +- [Apple Design Resources](https://developer.apple.com/design/resources/) + +--- + +**"Design is not just what it looks like and feels like. Design is how it works." - Steve Jobs** diff --git a/skills/apple-hig-designer/references/accessibility.md b/skills/apple-hig-designer/references/accessibility.md new file mode 100644 index 0000000..6aa35d7 --- /dev/null +++ b/skills/apple-hig-designer/references/accessibility.md @@ -0,0 +1 @@ +# iOS Accessibility - See SKILL.md for complete accessibility guide diff --git a/skills/apple-hig-designer/references/colors.md b/skills/apple-hig-designer/references/colors.md new file mode 100644 index 0000000..b9af87e --- /dev/null +++ b/skills/apple-hig-designer/references/colors.md @@ -0,0 +1 @@ +# iOS Colors - See SKILL.md for complete colors guide diff --git a/skills/apple-hig-designer/references/components.md b/skills/apple-hig-designer/references/components.md new file mode 100644 index 0000000..25f48af --- /dev/null +++ b/skills/apple-hig-designer/references/components.md @@ -0,0 +1,401 @@ +# iOS Components Catalog + +Complete reference for native iOS UI components following Apple Human Interface Guidelines. + +## Navigation Components + +### Navigation Bar + +Top bar for hierarchical navigation. + +```swift +NavigationStack { + List { + Text("Item 1") + Text("Item 2") + } + .navigationTitle("Title") + .navigationBarTitleDisplayMode(.large) // or .inline + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Add") { + // Action + } + } + } +} +``` + +**Guidelines:** +- Use `.large` for top-level views +- Use `.inline` for detail views +- Maximum 2-3 toolbar items +- Keep actions relevant to context + +### Tab Bar + +Bottom navigation for peer destinations. + +```swift +TabView { + HomeView() + .tabItem { + Label("Home", systemImage: "house") + } + + SearchView() + .tabItem { + Label("Search", systemImage: "magnifyingglass") + } +} +``` + +**Guidelines:** +- 3-5 tabs maximum +- Use SF Symbols for icons +- One-word labels +- Never hide or disable tabs + +### Toolbar + +Actions for current context. + +```swift +.toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Edit") { } + } + + ToolbarItem(placement: .bottomBar) { + Button("Delete") { } + } +} +``` + +## Controls + +### Button + +Primary action control. + +```swift +// Prominent (primary action) +Button("Continue") { } + .buttonStyle(.borderedProminent) + +// Bordered (secondary action) +Button("Cancel") { } + .buttonStyle(.bordered) + +// Plain (tertiary action) +Button("Learn More") { } + .buttonStyle(.plain) +``` + +### Toggle (Switch) + +Boolean control. + +```swift +Toggle("Enable notifications", isOn: $isEnabled) + .toggleStyle(.switch) +``` + +### Slider + +Continuous value selection. + +```swift +Slider(value: $volume, in: 0...100) + .accessibilityLabel("Volume") + .accessibilityValue("\(Int(volume))%") +``` + +### Picker + +Selection from multiple options. + +```swift +// Menu style +Picker("Size", selection: $size) { + Text("Small").tag("S") + Text("Medium").tag("M") + Text("Large").tag("L") +} +.pickerStyle(.menu) + +// Segmented (2-5 options) +Picker("View", selection: $viewType) { + Text("List").tag(0) + Text("Grid").tag(1) +} +.pickerStyle(.segmented) +``` + +### TextField + +Text input. + +```swift +TextField("Username", text: $username) + .textFieldStyle(.roundedBorder) + .textContentType(.username) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() +``` + +### SecureField + +Password input. + +```swift +SecureField("Password", text: $password) + .textFieldStyle(.roundedBorder) + .textContentType(.password) +``` + +## Content Views + +### List + +Scrollable rows of content. + +```swift +List { + Section("Today") { + ForEach(items) { item in + NavigationLink { + DetailView(item: item) + } label: { + HStack { + Image(systemName: item.icon) + Text(item.title) + } + } + } + .onDelete { indices in + items.remove(atOffsets: indices) + } + } +} +.listStyle(.insetGrouped) +``` + +**List Styles:** +- `.plain` - Edge-to-edge +- `.insetGrouped` - Rounded sections (iOS default) +- `.sidebar` - Navigation sidebar + +### ScrollView + +Custom scrollable content. + +```swift +ScrollView { + VStack(spacing: 16) { + ForEach(items) { item in + CardView(item: item) + } + } + .padding() +} +``` + +### Grid + +Multi-column layout. + +```swift +// LazyVGrid for vertical scrolling +LazyVGrid(columns: [ + GridItem(.adaptive(minimum: 150)) +]) { + ForEach(items) { item in + CardView(item: item) + } +} + +// LazyHGrid for horizontal scrolling +LazyHGrid(rows: [ + GridItem(.fixed(200)) +]) { + ForEach(items) { item in + CardView(item: item) + } +} +``` + +## Presentations + +### Sheet (Modal) + +Present content modally. + +```swift +.sheet(isPresented: $showSheet) { + NavigationStack { + DetailView() + .navigationTitle("Details") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + showSheet = false + } + } + } + } + .presentationDetents([.medium, .large]) +} +``` + +**Detents:** +- `.medium` - Half screen +- `.large` - Full screen +- Custom: `.height(400)` + +### Alert + +Important messages. + +```swift +.alert("Delete Item?", isPresented: $showAlert) { + Button("Delete", role: .destructive) { + deleteItem() + } + Button("Cancel", role: .cancel) { } +} message: { + Text("This action cannot be undone.") +} +``` + +### Confirmation Dialog + +Action selection. + +```swift +.confirmationDialog("Options", isPresented: $showOptions) { + Button("Edit") { } + Button("Share") { } + Button("Delete", role: .destructive) { } + Button("Cancel", role: .cancel) { } +} +``` + +## Indicators + +### Progress View + +Loading indicator. + +```swift +// Indeterminate +ProgressView() + +// Determinate +ProgressView(value: progress, total: 1.0) + +// With label +ProgressView("Loading...") { + // Optional current task + Text("5 of 10 items") +} +``` + +### Activity Indicator + +Spinning indicator. + +```swift +ProgressView() + .progressViewStyle(.circular) + .scaleEffect(1.5) +``` + +## Content Containers + +### Card View + +Contained content block. + +```swift +struct CardView: View { + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text("Title") + .font(.headline) + + Text("Description") + .font(.subheadline) + .foregroundColor(.secondary) + + Button("Action") { } + .buttonStyle(.borderedProminent) + } + .padding() + .background(Color(.systemBackground)) + .cornerRadius(12) + .shadow(color: .black.opacity(0.1), radius: 8) + } +} +``` + +### GroupBox + +Labeled group of views. + +```swift +GroupBox("Settings") { + Toggle("Notifications", isOn: $notifications) + Toggle("Location", isOn: $location) +} +``` + +### Form + +Settings and input grouping. + +```swift +Form { + Section("Account") { + TextField("Username", text: $username) + SecureField("Password", text: $password) + } + + Section("Preferences") { + Toggle("Notifications", isOn: $notifications) + Picker("Theme", selection: $theme) { + Text("Light").tag("light") + Text("Dark").tag("dark") + } + } +} +``` + +## Best Practices + +**Navigation:** +- Use NavigationStack for hierarchical +- Use TabView for flat (3-5 peers) +- Never combine TabView + Toolbar in same view + +**Forms:** +- Group related inputs +- Provide clear labels +- Show validation errors inline +- Use appropriate input types + +**Lists:** +- Use `.insetGrouped` style +- Support swipe actions +- Provide pull-to-refresh when relevant +- Use sections for organization + +**Modals:** +- Use for focused tasks +- Provide clear dismiss action +- Don't nest modals +- Use appropriate detent + +--- + +For more details: https://developer.apple.com/design/human-interface-guidelines/components diff --git a/skills/apple-hig-designer/references/layout_spacing.md b/skills/apple-hig-designer/references/layout_spacing.md new file mode 100644 index 0000000..6b4e0a9 --- /dev/null +++ b/skills/apple-hig-designer/references/layout_spacing.md @@ -0,0 +1 @@ +# iOS Layout & Spacing - See SKILL.md for complete layout guide diff --git a/skills/apple-hig-designer/references/typography.md b/skills/apple-hig-designer/references/typography.md new file mode 100644 index 0000000..90a1e07 --- /dev/null +++ b/skills/apple-hig-designer/references/typography.md @@ -0,0 +1 @@ +# iOS Typography - See SKILL.md for complete typography guide diff --git a/skills/apple-hig-designer/scripts/audit_accessibility.sh b/skills/apple-hig-designer/scripts/audit_accessibility.sh new file mode 100755 index 0000000..c8f4033 --- /dev/null +++ b/skills/apple-hig-designer/scripts/audit_accessibility.sh @@ -0,0 +1,208 @@ +#!/bin/bash + +# Apple HIG Designer - iOS Accessibility Audit +# Check iOS app accessibility compliance (VoiceOver, Dynamic Type, etc.) + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +MAGENTA='\033[0;35m' +NC='\033[0m' + +PASS_COUNT=0 +FAIL_COUNT=0 +WARNING_COUNT=0 + +print_success() { + echo -e "${GREEN}✓ PASS${NC} $1" + ((PASS_COUNT++)) +} + +print_error() { + echo -e "${RED}✗ FAIL${NC} $1" + ((FAIL_COUNT++)) +} + +print_warning() { + echo -e "${YELLOW}⚠ WARN${NC} $1" + ((WARNING_COUNT++)) +} + +print_info() { + echo -e "${BLUE}ℹ INFO${NC} $1" +} + +print_section() { + echo "" + echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${MAGENTA}$1${NC}" + echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +} + +echo "" +echo "╔════════════════════════════════════════════════════════════╗" +echo "║ ║" +echo "║ Apple HIG Designer - Accessibility Audit ║" +echo "║ ║" +echo "╚════════════════════════════════════════════════════════════╝" +echo "" + +if [ -z "$1" ]; then + print_info "Usage: $0 " + exit 1 +fi + +TARGET="$1" + +if [ ! -e "$TARGET" ]; then + print_error "Target not found: $TARGET" + exit 1 +fi + +# VoiceOver Support +print_section "1. VOICEOVER SUPPORT" + +check_voiceover() { + if grep -q 'Image(systemName:' "$1"; then + if grep -q '\.accessibilityLabel' "$1"; then + print_success "Images have accessibility labels" + else + print_error "Images missing accessibility labels" + echo " Fix: .accessibilityLabel(\"Description\")" + fi + fi + + if grep -q '\.accessibilityHint' "$1"; then + print_success "Accessibility hints provided" + fi + + if grep -q '\.accessibilityElement(children: .combine)' "$1"; then + print_success "Grouping accessibility elements" + fi +} + +# Dynamic Type +print_section "2. DYNAMIC TYPE" + +check_dynamic_type() { + if grep -q '\.font(.body)\|\.font(.headline)\|\.font(.title)' "$1"; then + print_success "Using system text styles (Dynamic Type supported)" + else + print_error "Not using system text styles" + echo " Fix: Use .font(.body) instead of .font(.system(size: 17))" + fi + + if grep -q '\.font(.custom(' "$1"; then + if grep -q 'relativeTo:' "$1"; then + print_success "Custom fonts support Dynamic Type" + else + print_error "Custom fonts don't support Dynamic Type" + echo " Fix: .font(.custom(\"Font\", size: 17, relativeTo: .body))" + fi + fi +} + +# Color Contrast +print_section "3. COLOR CONTRAST" + +check_color_contrast() { + if grep -q 'Color(.label)\|Color(.secondaryLabel)' "$1"; then + print_success "Using semantic text colors (good contrast)" + else + print_warning "Verify color contrast ratios (4.5:1 minimum)" + fi + + if grep -q '@Environment(.*colorSchemeContrast)' "$1"; then + print_success "Supporting Increase Contrast mode" + else + print_warning "Consider supporting Increase Contrast" + echo " Tip: @Environment(\\.colorSchemeContrast) var contrast" + fi +} + +# Reduce Motion +print_section "4. REDUCE MOTION" + +check_reduce_motion() { + if grep -q '@Environment(.*accessibilityReduceMotion)' "$1"; then + print_success "Respecting Reduce Motion preference" + else + if grep -q 'withAnimation\|\.animation' "$1"; then + print_error "Animations present but not respecting Reduce Motion" + echo " Fix: @Environment(\\.accessibilityReduceMotion) var reduceMotion" + fi + fi +} + +# Touch Targets +print_section "5. TOUCH TARGETS" + +check_touch_targets() { + if grep -q '\.frame.*minHeight.*44\|height.*44' "$1"; then + print_success "Minimum touch target (44pt) specified" + else + if grep -q 'Button\|.onTapGesture' "$1"; then + print_error "Touch targets may be too small" + echo " Fix: .frame(minWidth: 44, minHeight: 44)" + fi + fi +} + +# Run checks +if [ -f "$TARGET" ]; then + if [[ "$TARGET" == *.swift ]]; then + check_voiceover "$TARGET" + check_dynamic_type "$TARGET" + check_color_contrast "$TARGET" + check_reduce_motion "$TARGET" + check_touch_targets "$TARGET" + fi +elif [ -d "$TARGET" ]; then + swift_files=$(find "$TARGET" -name "*.swift" 2>/dev/null) + for file in $swift_files; do + print_info "Checking: $file" + check_voiceover "$file" + check_dynamic_type "$file" + check_color_contrast "$file" + check_reduce_motion "$file" + check_touch_targets "$file" + echo "" + done +fi + +# Summary +echo "" +echo "╔════════════════════════════════════════════════════════════╗" +echo "║ Accessibility Summary ║" +echo "╚════════════════════════════════════════════════════════════╝" +echo "" +echo -e "${GREEN}✓ Passed: $PASS_COUNT${NC}" +echo -e "${RED}✗ Failed: $FAIL_COUNT${NC}" +echo -e "${YELLOW}⚠ Warnings: $WARNING_COUNT${NC}" +echo "" + +TOTAL=$((PASS_COUNT + FAIL_COUNT)) +if [ $TOTAL -gt 0 ]; then + SCORE=$(( (PASS_COUNT * 100) / TOTAL )) + echo "Accessibility Score: $SCORE%" + echo "" +fi + +echo "" +print_info "Testing Recommendations:" +echo " 1. Test with VoiceOver enabled (Settings > Accessibility > VoiceOver)" +echo " 2. Test with largest Dynamic Type size" +echo " 3. Test with Increase Contrast enabled" +echo " 4. Test with Reduce Motion enabled" +echo " 5. Test with Reduce Transparency enabled" +echo " 6. Use Accessibility Inspector in Xcode" +echo "" +print_info "Resources:" +echo " - Accessibility Inspector: Xcode > Open Developer Tool" +echo " - Apple Accessibility: https://www.apple.com/accessibility/" +echo "" + +[ $FAIL_COUNT -gt 0 ] && exit 1 || exit 0 diff --git a/skills/apple-hig-designer/scripts/generate_ios_component.sh b/skills/apple-hig-designer/scripts/generate_ios_component.sh new file mode 100755 index 0000000..d8083d5 --- /dev/null +++ b/skills/apple-hig-designer/scripts/generate_ios_component.sh @@ -0,0 +1,348 @@ +#!/bin/bash + +# Apple HIG Designer - iOS Component Generator +# Generate SwiftUI and UIKit components following Apple Human Interface Guidelines + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Helper functions +print_success() { + echo -e "${GREEN}✓ $1${NC}" +} + +print_error() { + echo -e "${RED}✗ $1${NC}" +} + +print_info() { + echo -e "${BLUE}ℹ $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}⚠ $1${NC}" +} + +prompt_input() { + local prompt="$1" + local var_name="$2" + local required="${3:-false}" + + while true; do + echo -e "${BLUE}${prompt}${NC}" + read -r input + + if [ -z "$input" ] && [ "$required" = true ]; then + print_error "This field is required." + continue + fi + + eval "$var_name='$input'" + break + done +} + +prompt_select() { + local prompt="$1" + local var_name="$2" + shift 2 + local options=("$@") + + echo -e "${BLUE}${prompt}${NC}" + PS3="Select (1-${#options[@]}): " + select opt in "${options[@]}"; do + if [ -n "$opt" ]; then + eval "$var_name='$opt'" + break + else + print_error "Invalid selection. Try again." + fi + done +} + +# Banner +echo "" +echo "╔════════════════════════════════════════════════════════════╗" +echo "║ ║" +echo "║ Apple HIG Designer - Component Generator ║" +echo "║ ║" +echo "╚════════════════════════════════════════════════════════════╝" +echo "" + +# Step 1: Framework +print_info "Step 1/6: Framework" +prompt_select "Which framework?" FRAMEWORK \ + "SwiftUI" \ + "UIKit" + +# Step 2: Component Type +print_info "Step 2/6: Component Type" +prompt_select "What type of component?" COMPONENT_TYPE \ + "Button" \ + "List/TableView" \ + "Card" \ + "Sheet/Modal" \ + "Form" \ + "NavigationView" \ + "TabView" \ + "Custom" + +# Step 3: Component Name +print_info "Step 3/6: Component Name" +prompt_input "Component name (e.g., UserProfileView):" COMPONENT_NAME true + +# Step 4: Features +print_info "Step 4/6: Features (comma-separated)" +echo -e "${BLUE}Select features to include:${NC}" +echo " - accessibility (VoiceOver, Dynamic Type)" +echo " - darkmode (Semantic colors)" +echo " - animations (Standard iOS animations)" +echo " - haptics (Haptic feedback)" +read -r FEATURES + +# Step 5: Platform +print_info "Step 5/6: Platform" +prompt_select "Target platform?" PLATFORM \ + "iOS" \ + "iOS + iPadOS" \ + "iOS + watchOS" \ + "All (iOS, iPadOS, macOS, watchOS)" + +# Step 6: Output Directory +print_info "Step 6/6: Output Location" +prompt_input "Output directory (default: ./Components):" OUTPUT_DIR +OUTPUT_DIR=${OUTPUT_DIR:-"./Components"} + +# Create output directory +mkdir -p "$OUTPUT_DIR" + +# Generate based on framework +case $FRAMEWORK in + "SwiftUI") + generate_swiftui_component + ;; + "UIKit") + generate_uikit_component + ;; +esac + +generate_swiftui_component() { + local file_path="$OUTPUT_DIR/$COMPONENT_NAME.swift" + + cat > "$file_path" << 'EOF' +import SwiftUI + +/// COMPONENT_NAME +/// +/// Description of what this component does +/// Follows Apple Human Interface Guidelines +struct COMPONENT_NAME: View { + PROPERTIES + + var body: some View { + COMPONENT_BODY + } +} + +PREVIEW +EOF + + # Add properties based on component type + case $COMPONENT_TYPE in + "Button") + sed -i 's/PROPERTIES/\/\/ Button properties\n let title: String\n let action: () -> Void/' "$file_path" + sed -i 's/COMPONENT_BODY/Button(action: action) {\n Text(title)\n }\n .buttonStyle(.borderedProminent)\n ACCESSIBILITY_MODIFIERS/' "$file_path" + ;; + "List/TableView") + sed -i 's/PROPERTIES/\/\/ List properties\n let items: [String]/' "$file_path" + sed -i 's/COMPONENT_BODY/List(items, id: \\.self) { item in\n Text(item)\n }\n .listStyle(.insetGrouped)\n ACCESSIBILITY_MODIFIERS/' "$file_path" + ;; + "Card") + sed -i 's/PROPERTIES/\/\/ Card properties\n let title: String\n let description: String/' "$file_path" + sed -i 's/COMPONENT_BODY/VStack(alignment: .leading, spacing: 12) {\n Text(title)\n .font(.headline)\n \n Text(description)\n .font(.subheadline)\n .foregroundColor(.secondary)\n }\n .padding()\n .background(Color(.systemBackground))\n .cornerRadius(12)\n .shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 4)\n ACCESSIBILITY_MODIFIERS/' "$file_path" + ;; + "Sheet/Modal") + sed -i 's/PROPERTIES/@State private var isPresented = false/' "$file_path" + sed -i 's/COMPONENT_BODY/Button("Show Sheet") {\n isPresented = true\n }\n .sheet(isPresented: $isPresented) {\n NavigationStack {\n Text("Sheet Content")\n .navigationTitle("Title")\n .navigationBarTitleDisplayMode(.inline)\n .toolbar {\n ToolbarItem(placement: .cancellationAction) {\n Button("Cancel") {\n isPresented = false\n }\n }\n }\n }\n .presentationDetents([.medium, .large])\n }/' "$file_path" + ;; + "NavigationView") + sed -i 's/PROPERTIES/\/\/ Navigation properties\n @State private var path = NavigationPath()/' "$file_path" + sed -i 's/COMPONENT_BODY/NavigationStack(path: $path) {\n List {\n NavigationLink("Item 1", value: "Detail 1")\n NavigationLink("Item 2", value: "Detail 2")\n }\n .navigationTitle("Title")\n .navigationBarTitleDisplayMode(.large)\n .navigationDestination(for: String.self) { value in\n Text(value)\n }\n }/' "$file_path" + ;; + "TabView") + sed -i 's/PROPERTIES/@State private var selectedTab = 0/' "$file_path" + sed -i 's/COMPONENT_BODY/TabView(selection: $selectedTab) {\n Text("Home")\n .tabItem {\n Label("Home", systemImage: "house")\n }\n .tag(0)\n \n Text("Search")\n .tabItem {\n Label("Search", systemImage: "magnifyingglass")\n }\n .tag(1)\n \n Text("Profile")\n .tabItem {\n Label("Profile", systemImage: "person")\n }\n .tag(2)\n }/' "$file_path" + ;; + *) + sed -i 's/PROPERTIES/\/\/ Component properties/' "$file_path" + sed -i 's/COMPONENT_BODY/Text("Custom Component")\n .font(.body)\n ACCESSIBILITY_MODIFIERS/' "$file_path" + ;; + esac + + # Add accessibility modifiers if requested + if [[ $FEATURES == *"accessibility"* ]]; then + sed -i 's/ACCESSIBILITY_MODIFIERS/.accessibilityLabel("Component label")\n .accessibilityHint("Component hint")/' "$file_path" + else + sed -i 's/ACCESSIBILITY_MODIFIERS//' "$file_path" + fi + + # Add preview + sed -i "s/PREVIEW/#Preview {\n COMPONENT_NAME()\n}/" "$file_path" + + # Replace component name + sed -i "s/COMPONENT_NAME/$COMPONENT_NAME/g" "$file_path" + + print_success "Created SwiftUI component: $file_path" + + # Generate test file if needed + if [[ $FEATURES == *"testing"* ]]; then + generate_test_file_swiftui + fi +} + +generate_uikit_component() { + local file_path="$OUTPUT_DIR/$COMPONENT_NAME.swift" + + cat > "$file_path" << 'EOF' +import UIKit + +/// COMPONENT_NAME +/// +/// Description of what this component does +/// Follows Apple Human Interface Guidelines +class COMPONENT_NAME: UIView { + + // MARK: - Properties + PROPERTIES + + // MARK: - Initialization + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupView() + } + + // MARK: - Setup + + private func setupView() { + SETUP_CODE + setupAccessibility() + } + + private func setupAccessibility() { + ACCESSIBILITY_SETUP + } + + // MARK: - Layout + + override func layoutSubviews() { + super.layoutSubviews() + // Layout code here + } +} +EOF + + # Add properties based on component type + case $COMPONENT_TYPE in + "Button") + sed -i 's/PROPERTIES/private let button: UIButton = {\n let button = UIButton(type: .system)\n button.translatesAutoresizingMaskIntoConstraints = false\n button.configuration = .filled()\n return button\n }()/' "$file_path" + sed -i 's/SETUP_CODE/addSubview(button)\n \n NSLayoutConstraint.activate([\n button.centerXAnchor.constraint(equalTo: centerXAnchor),\n button.centerYAnchor.constraint(equalTo: centerYAnchor),\n button.heightAnchor.constraint(greaterThanOrEqualToConstant: 44)\n ])/' "$file_path" + ;; + "List/TableView") + sed -i 's/PROPERTIES/private let tableView: UITableView = {\n let table = UITableView(frame: .zero, style: .insetGrouped)\n table.translatesAutoresizingMaskIntoConstraints = false\n return table\n }()\n \n private var items: [String] = []/' "$file_path" + sed -i 's/SETUP_CODE/addSubview(tableView)\n tableView.delegate = self\n tableView.dataSource = self\n \n NSLayoutConstraint.activate([\n tableView.topAnchor.constraint(equalTo: topAnchor),\n tableView.leadingAnchor.constraint(equalTo: leadingAnchor),\n tableView.trailingAnchor.constraint(equalTo: trailingAnchor),\n tableView.bottomAnchor.constraint(equalTo: bottomAnchor)\n ])/' "$file_path" + ;; + *) + sed -i 's/PROPERTIES/\/\/ Add component properties here/' "$file_path" + sed -i 's/SETUP_CODE/\/\/ Setup UI components/' "$file_path" + ;; + esac + + # Add accessibility setup + if [[ $FEATURES == *"accessibility"* ]]; then + sed -i 's/ACCESSIBILITY_SETUP/isAccessibilityElement = true\n accessibilityLabel = "Component label"\n accessibilityHint = "Component hint"\n accessibilityTraits = .button/' "$file_path" + else + sed -i 's/ACCESSIBILITY_SETUP/\/\/ Configure accessibility/' "$file_path" + fi + + # Replace component name + sed -i "s/COMPONENT_NAME/$COMPONENT_NAME/g" "$file_path" + + print_success "Created UIKit component: $file_path" +} + +generate_test_file_swiftui() { + local test_file="$OUTPUT_DIR/${COMPONENT_NAME}Tests.swift" + + cat > "$test_file" << 'EOF' +import XCTest +import SwiftUI +@testable import YourApp + +final class COMPONENT_NAMETests: XCTestCase { + + func testComponentRenders() { + let view = COMPONENT_NAME() + XCTAssertNotNil(view) + } + + func testAccessibility() { + // Test VoiceOver labels + // Test Dynamic Type support + } +} +EOF + + sed -i "s/COMPONENT_NAME/$COMPONENT_NAME/g" "$test_file" + print_success "Created test file: $test_file" +} + +# Summary +echo "" +echo "╔════════════════════════════════════════════════════════════╗" +echo "║ Generation Complete ║" +echo "╚════════════════════════════════════════════════════════════╝" +echo "" +print_success "Component: $COMPONENT_NAME" +print_success "Framework: $FRAMEWORK" +print_success "Type: $COMPONENT_TYPE" +print_success "Platform: $PLATFORM" +print_success "Location: $OUTPUT_DIR" +echo "" +print_info "Files created:" +echo " - $COMPONENT_NAME.swift" +if [[ $FEATURES == *"testing"* ]]; then + echo " - ${COMPONENT_NAME}Tests.swift" +fi +echo "" +print_info "Apple HIG Guidelines Applied:" +echo " ✓ Minimum tap target: 44x44 points" +echo " ✓ System fonts (San Francisco)" +echo " ✓ Semantic colors (dark mode support)" +if [[ $FEATURES == *"accessibility"* ]]; then + echo " ✓ VoiceOver support" + echo " ✓ Dynamic Type support" +fi +if [[ $FEATURES == *"haptics"* ]]; then + echo " ✓ Haptic feedback" +fi +echo "" +print_info "Next steps:" +echo " 1. Review generated code" +echo " 2. Add component to your Xcode project" +echo " 3. Customize properties and logic" +echo " 4. Test with VoiceOver" +echo " 5. Test in light and dark mode" +echo " 6. Test with different Dynamic Type sizes" +echo "" diff --git a/skills/apple-hig-designer/scripts/validate_design.sh b/skills/apple-hig-designer/scripts/validate_design.sh new file mode 100755 index 0000000..6eefb17 --- /dev/null +++ b/skills/apple-hig-designer/scripts/validate_design.sh @@ -0,0 +1,393 @@ +#!/bin/bash + +# Apple HIG Designer - Design Validation +# Validate Swift code against Apple Human Interface Guidelines + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +MAGENTA='\033[0;35m' +NC='\033[0m' # No Color + +# Counters +PASS_COUNT=0 +FAIL_COUNT=0 +WARNING_COUNT=0 + +# Helper functions +print_success() { + echo -e "${GREEN}✓ PASS${NC} $1" + ((PASS_COUNT++)) +} + +print_error() { + echo -e "${RED}✗ FAIL${NC} $1" + ((FAIL_COUNT++)) +} + +print_warning() { + echo -e "${YELLOW}⚠ WARN${NC} $1" + ((WARNING_COUNT++)) +} + +print_info() { + echo -e "${BLUE}ℹ INFO${NC} $1" +} + +print_section() { + echo "" + echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${MAGENTA}$1${NC}" + echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +} + +# Banner +echo "" +echo "╔════════════════════════════════════════════════════════════╗" +echo "║ ║" +echo "║ Apple HIG Designer - Design Validation ║" +echo "║ ║" +echo "╚════════════════════════════════════════════════════════════╝" +echo "" + +# Get target +if [ -z "$1" ]; then + print_info "Usage: $0 " + print_info "Example: $0 ContentView.swift" + print_info "Example: $0 Views/" + exit 1 +fi + +TARGET="$1" + +# Check if target exists +if [ ! -e "$TARGET" ]; then + print_error "Target not found: $TARGET" + exit 1 +fi + +# Section 1: Typography +print_section "1. TYPOGRAPHY" + +check_system_fonts() { + # Check for system font usage + if grep -q '\.font(' "$1"; then + if grep -q '\.font(.body)\|\.font(.title)\|\.font(.headline)' "$1"; then + print_success "Using system text styles (Dynamic Type)" + else + print_warning "Consider using system text styles (.body, .headline, etc.)" + fi + fi + + # Check for hard-coded font sizes + if grep -q 'Font.system(size:' "$1"; then + print_warning "Hard-coded font sizes found - consider Dynamic Type" + echo " Tip: Use .font(.body) instead of .font(.system(size: 17))" + fi +} + +# Section 2: Colors +print_section "2. COLORS & DARK MODE" + +check_semantic_colors() { + # Check for semantic color usage + if grep -q 'Color(.label)\|Color(.systemBackground)\|Color(.secondary)' "$1"; then + print_success "Using semantic colors (dark mode support)" + else + if grep -q 'Color(' "$1"; then + print_warning "Consider using semantic colors for dark mode support" + echo " Use: Color(.label) instead of Color.black" + fi + fi + + # Check for hard-coded colors + if grep -q '#[0-9A-Fa-f]\{6\}\|Color.black\|Color.white' "$1"; then + print_error "Hard-coded colors found - use semantic colors" + echo " Fix: Use Color(.label), Color(.systemBackground), etc." + fi +} + +# Section 3: Accessibility +print_section "3. ACCESSIBILITY" + +check_accessibility_labels() { + # Check for accessibility labels + if grep -q 'Image(systemName:' "$1"; then + if grep -q '\.accessibilityLabel' "$1"; then + print_success "SF Symbols have accessibility labels" + else + print_error "SF Symbols missing accessibility labels" + echo " Fix: Add .accessibilityLabel(\"Description\")" + fi + fi + + # Check for VoiceOver support + if grep -q '\.accessibilityLabel\|\.accessibilityHint\|\.accessibilityValue' "$1"; then + print_success "VoiceOver support implemented" + else + print_warning "Consider adding VoiceOver support" + fi +} + +# Section 4: Touch Targets +print_section "4. TOUCH TARGETS" + +check_touch_targets() { + # Check for minimum tap targets + if grep -q '\.frame(.*height.*44\|minHeight.*44' "$1"; then + print_success "Minimum touch target size (44pt) specified" + else + if grep -q 'Button\|.onTapGesture' "$1"; then + print_warning "Verify touch targets are at least 44x44 points" + echo " Tip: .frame(minWidth: 44, minHeight: 44)" + fi + fi +} + +# Section 5: Spacing +print_section "5. SPACING & LAYOUT" + +check_spacing() { + # Check for 8pt grid usage + if grep -q '\.padding([0-9]*)\|\.spacing([0-9]*)' "$1"; then + # Extract padding values + paddings=$(grep -o '\.padding([0-9]*)' "$1" | grep -o '[0-9]*') + invalid=false + for pad in $paddings; do + if [ $((pad % 8)) -ne 0 ]; then + invalid=true + break + fi + done + + if [ "$invalid" = false ]; then + print_success "Following 8pt grid system" + else + print_warning "Consider using 8pt grid (8, 16, 24, 32, etc.)" + fi + fi + + # Check for safe area usage + if grep -q '\.ignoresSafeArea' "$1"; then + print_warning "Ignoring safe areas - ensure intentional" + echo " Tip: Respect safe areas for better device compatibility" + fi +} + +# Section 6: Navigation +print_section "6. NAVIGATION" + +check_navigation() { + # Check for navigation best practices + if grep -q 'NavigationStack\|NavigationView' "$1"; then + if grep -q '\.navigationTitle' "$1"; then + print_success "Navigation titles present" + else + print_error "NavigationStack missing .navigationTitle" + fi + + # Check for navigation bar mode + if grep -q '\.navigationBarTitleDisplayMode(.large)' "$1"; then + print_success "Using large titles for top-level views" + fi + fi + + # Check for TabView + if grep -q 'TabView' "$1"; then + # Count tabItems + tab_count=$(grep -c '\.tabItem' "$1") + if [ "$tab_count" -ge 3 ] && [ "$tab_count" -le 5 ]; then + print_success "TabView has appropriate number of tabs ($tab_count)" + elif [ "$tab_count" -gt 5 ]; then + print_error "Too many tabs ($tab_count) - maximum 5 recommended" + elif [ "$tab_count" -lt 3 ]; then + print_warning "Consider if TabView is appropriate for $tab_count tabs" + fi + fi +} + +# Section 7: Buttons +print_section "7. BUTTONS & CONTROLS" + +check_buttons() { + # Check for button styles + if grep -q 'Button(' "$1"; then + if grep -q '\.buttonStyle' "$1"; then + print_success "Using button styles" + + # Check for button hierarchy + if grep -q '\.buttonStyle(.borderedProminent)' "$1"; then + prominent_count=$(grep -c '\.buttonStyle(.borderedProminent)' "$1") + if [ "$prominent_count" -eq 1 ]; then + print_success "Single prominent button (good hierarchy)" + else + print_warning "Multiple prominent buttons - ensure clear hierarchy" + fi + fi + else + print_warning "Consider using .buttonStyle for consistent appearance" + fi + + # Check button labels + if grep -q 'Button("' "$1"; then + # Extract button texts + if grep -q 'Button("[A-Z]' "$1"; then + print_success "Button labels start with capital letters" + fi + fi + fi +} + +# Section 8: Lists +print_section "8. LISTS & TABLES" + +check_lists() { + # Check for list styles + if grep -q 'List' "$1"; then + if grep -q '\.listStyle' "$1"; then + if grep -q '\.listStyle(.insetGrouped)' "$1"; then + print_success "Using iOS standard .insetGrouped list style" + fi + else + print_warning "Consider specifying .listStyle(.insetGrouped)" + fi + fi +} + +# Section 9: Animations +print_section "9. ANIMATIONS & MOTION" + +check_animations() { + # Check for spring animations + if grep -q 'withAnimation' "$1"; then + if grep -q '\.spring()\|\.easeInOut' "$1"; then + print_success "Using iOS-style animations" + fi + fi + + # Check for reduce motion + if grep -q '@Environment(.*accessibilityReduceMotion)' "$1"; then + print_success "Respecting Reduce Motion preference" + else + if grep -q 'withAnimation\|\.animation' "$1"; then + print_warning "Consider respecting Reduce Motion accessibility setting" + echo " Tip: @Environment(\\.accessibilityReduceMotion) var reduceMotion" + fi + fi +} + +# Section 10: SF Symbols +print_section "10. SF SYMBOLS" + +check_sf_symbols() { + # Check for SF Symbols usage + if grep -q 'Image(systemName:' "$1"; then + print_success "Using SF Symbols" + + # Check for proper sizing + if grep -q '\.font(.title)\|\.imageScale' "$1"; then + print_success "SF Symbols are properly sized" + else + print_warning "Consider sizing SF Symbols with .font() or .imageScale()" + fi + fi + + # Check for custom images that should be SF Symbols + common_icons="heart|star|person|home|settings|search|share" + if grep -E "Image\\(\"($common_icons)" "$1"; then + print_warning "Consider using SF Symbols instead of custom icons" + fi +} + +# Run checks on files +if [ -f "$TARGET" ]; then + # Single file + if [[ "$TARGET" == *.swift ]]; then + check_system_fonts "$TARGET" + check_semantic_colors "$TARGET" + check_accessibility_labels "$TARGET" + check_touch_targets "$TARGET" + check_spacing "$TARGET" + check_navigation "$TARGET" + check_buttons "$TARGET" + check_lists "$TARGET" + check_animations "$TARGET" + check_sf_symbols "$TARGET" + else + print_error "File is not a Swift file: $TARGET" + exit 1 + fi +elif [ -d "$TARGET" ]; then + # Directory - find Swift files + swift_files=$(find "$TARGET" -name "*.swift" 2>/dev/null) + + if [ -z "$swift_files" ]; then + print_error "No Swift files found in $TARGET" + exit 1 + fi + + for file in $swift_files; do + print_info "Checking: $file" + check_system_fonts "$file" + check_semantic_colors "$file" + check_accessibility_labels "$file" + check_touch_targets "$file" + check_spacing "$file" + echo "" + done +fi + +# Summary +echo "" +echo "╔════════════════════════════════════════════════════════════╗" +echo "║ Validation Summary ║" +echo "╚════════════════════════════════════════════════════════════╝" +echo "" +echo -e "${GREEN}✓ Passed: $PASS_COUNT${NC}" +echo -e "${RED}✗ Failed: $FAIL_COUNT${NC}" +echo -e "${YELLOW}⚠ Warnings: $WARNING_COUNT${NC}" +echo "" + +# Calculate score +TOTAL=$((PASS_COUNT + FAIL_COUNT)) +if [ $TOTAL -gt 0 ]; then + SCORE=$(( (PASS_COUNT * 100) / TOTAL )) + echo "HIG Compliance Score: $SCORE%" + echo "" + + if [ $SCORE -ge 90 ]; then + echo -e "${GREEN}Excellent! Your design follows Apple HIG.${NC}" + elif [ $SCORE -ge 70 ]; then + echo -e "${YELLOW}Good, but needs improvements.${NC}" + else + echo -e "${RED}Needs significant improvements to match Apple HIG.${NC}" + fi +fi + +echo "" +print_info "Apple HIG Recommendations:" +echo " 1. Use system fonts with Dynamic Type" +echo " 2. Use semantic colors for dark mode" +echo " 3. Add VoiceOver labels to all images" +echo " 4. Ensure 44x44pt minimum touch targets" +echo " 5. Follow 8pt grid system" +echo " 6. Use large titles for top-level navigation" +echo " 7. Limit TabView to 3-5 tabs" +echo " 8. Use .borderedProminent for primary actions only" +echo "" +print_info "Resources:" +echo " - Apple HIG: https://developer.apple.com/design/human-interface-guidelines/" +echo " - SF Symbols: https://developer.apple.com/sf-symbols/" +echo " - WWDC Videos: https://developer.apple.com/videos/" +echo "" + +# Exit code based on failures +if [ $FAIL_COUNT -gt 0 ]; then + exit 1 +else + exit 0 +fi