Initial commit
This commit is contained in:
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