Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -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.
|
||||||
76
plugin.lock.json
Normal file
76
plugin.lock.json
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
946
skills/apple-hig-designer/SKILL.md
Normal file
946
skills/apple-hig-designer/SKILL.md
Normal file
@@ -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**
|
||||||
1
skills/apple-hig-designer/references/accessibility.md
Normal file
1
skills/apple-hig-designer/references/accessibility.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# iOS Accessibility - See SKILL.md for complete accessibility guide
|
||||||
1
skills/apple-hig-designer/references/colors.md
Normal file
1
skills/apple-hig-designer/references/colors.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# iOS Colors - See SKILL.md for complete colors guide
|
||||||
401
skills/apple-hig-designer/references/components.md
Normal file
401
skills/apple-hig-designer/references/components.md
Normal file
@@ -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
|
||||||
1
skills/apple-hig-designer/references/layout_spacing.md
Normal file
1
skills/apple-hig-designer/references/layout_spacing.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# iOS Layout & Spacing - See SKILL.md for complete layout guide
|
||||||
1
skills/apple-hig-designer/references/typography.md
Normal file
1
skills/apple-hig-designer/references/typography.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# iOS Typography - See SKILL.md for complete typography guide
|
||||||
208
skills/apple-hig-designer/scripts/audit_accessibility.sh
Executable file
208
skills/apple-hig-designer/scripts/audit_accessibility.sh
Executable file
@@ -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 <file.swift|directory>"
|
||||||
|
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
|
||||||
348
skills/apple-hig-designer/scripts/generate_ios_component.sh
Executable file
348
skills/apple-hig-designer/scripts/generate_ios_component.sh
Executable file
@@ -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 ""
|
||||||
393
skills/apple-hig-designer/scripts/validate_design.sh
Executable file
393
skills/apple-hig-designer/scripts/validate_design.sh
Executable file
@@ -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 <file.swift|directory>"
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user