mirror of
https://github.com/wshobson/agents.git
synced 2026-03-18 17:47:16 +00:00
New plugin covering mobile (iOS, Android, React Native) and web applications with modern design patterns, accessibility, and design systems. Components: - 9 skills: design-system-patterns, accessibility-compliance, responsive-design, mobile-ios-design, mobile-android-design, react-native-design, web-component-design, interaction-design, visual-design-foundations - 4 commands: design-review, create-component, accessibility-audit, design-system-setup - 3 agents: ui-designer, accessibility-expert, design-system-architect Marketplace updated: - Version bumped to 1.3.4 - 102 agents (+3), 116 skills (+9)
530 lines
13 KiB
Markdown
530 lines
13 KiB
Markdown
# iOS Human Interface Guidelines Patterns
|
|
|
|
## Layout and Spacing
|
|
|
|
### Standard Margins and Padding
|
|
|
|
```swift
|
|
// System standard margins
|
|
private let standardMargin: CGFloat = 16
|
|
private let compactMargin: CGFloat = 8
|
|
private let largeMargin: CGFloat = 24
|
|
|
|
// Content insets following HIG
|
|
extension EdgeInsets {
|
|
static let standard = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)
|
|
static let listRow = EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)
|
|
static let card = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)
|
|
}
|
|
```
|
|
|
|
### Safe Area Handling
|
|
|
|
```swift
|
|
struct SafeAreaAwareView: View {
|
|
var body: some View {
|
|
ScrollView {
|
|
LazyVStack(spacing: 16) {
|
|
ForEach(items) { item in
|
|
ItemRow(item: item)
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
}
|
|
.safeAreaInset(edge: .bottom) {
|
|
// Floating action area
|
|
HStack {
|
|
Button("Cancel") { }
|
|
.buttonStyle(.bordered)
|
|
Spacer()
|
|
Button("Confirm") { }
|
|
.buttonStyle(.borderedProminent)
|
|
}
|
|
.padding()
|
|
.background(.regularMaterial)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Adaptive Layouts
|
|
|
|
```swift
|
|
struct AdaptiveGridView: View {
|
|
@Environment(\.horizontalSizeClass) private var sizeClass
|
|
|
|
private var columns: [GridItem] {
|
|
switch sizeClass {
|
|
case .compact:
|
|
return [GridItem(.flexible())]
|
|
case .regular:
|
|
return [
|
|
GridItem(.flexible()),
|
|
GridItem(.flexible()),
|
|
GridItem(.flexible())
|
|
]
|
|
default:
|
|
return [GridItem(.flexible())]
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
LazyVGrid(columns: columns, spacing: 16) {
|
|
ForEach(items) { item in
|
|
ItemCard(item: item)
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Typography Hierarchy
|
|
|
|
### System Font Styles
|
|
|
|
```swift
|
|
// HIG-compliant typography scale
|
|
struct Typography {
|
|
// Titles
|
|
static let largeTitle = Font.largeTitle.weight(.bold) // 34pt bold
|
|
static let title = Font.title.weight(.semibold) // 28pt semibold
|
|
static let title2 = Font.title2.weight(.semibold) // 22pt semibold
|
|
static let title3 = Font.title3.weight(.semibold) // 20pt semibold
|
|
|
|
// Headlines and body
|
|
static let headline = Font.headline // 17pt semibold
|
|
static let body = Font.body // 17pt regular
|
|
static let callout = Font.callout // 16pt regular
|
|
|
|
// Supporting text
|
|
static let subheadline = Font.subheadline // 15pt regular
|
|
static let footnote = Font.footnote // 13pt regular
|
|
static let caption = Font.caption // 12pt regular
|
|
static let caption2 = Font.caption2 // 11pt regular
|
|
}
|
|
```
|
|
|
|
### Custom Font with Dynamic Type
|
|
|
|
```swift
|
|
extension Font {
|
|
static func customBody(_ name: String) -> Font {
|
|
.custom(name, size: 17, relativeTo: .body)
|
|
}
|
|
|
|
static func customHeadline(_ name: String) -> Font {
|
|
.custom(name, size: 17, relativeTo: .headline)
|
|
.weight(.semibold)
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
Text("Custom styled text")
|
|
.font(.customBody("Avenir Next"))
|
|
```
|
|
|
|
## Color System
|
|
|
|
### Semantic Colors
|
|
|
|
```swift
|
|
// Use semantic colors for automatic light/dark mode support
|
|
extension Color {
|
|
// Labels
|
|
static let primaryLabel = Color.primary
|
|
static let secondaryLabel = Color.secondary
|
|
static let tertiaryLabel = Color(uiColor: .tertiaryLabel)
|
|
|
|
// Backgrounds
|
|
static let systemBackground = Color(uiColor: .systemBackground)
|
|
static let secondaryBackground = Color(uiColor: .secondarySystemBackground)
|
|
static let groupedBackground = Color(uiColor: .systemGroupedBackground)
|
|
|
|
// Fills
|
|
static let primaryFill = Color(uiColor: .systemFill)
|
|
static let secondaryFill = Color(uiColor: .secondarySystemFill)
|
|
|
|
// Separators
|
|
static let separator = Color(uiColor: .separator)
|
|
static let opaqueSeparator = Color(uiColor: .opaqueSeparator)
|
|
}
|
|
```
|
|
|
|
### Tint Colors
|
|
|
|
```swift
|
|
// App-wide tint color
|
|
struct AppColors {
|
|
static let primary = Color.blue
|
|
static let secondary = Color.purple
|
|
static let success = Color.green
|
|
static let warning = Color.orange
|
|
static let error = Color.red
|
|
|
|
// Semantic tints
|
|
static let interactive = Color.accentColor
|
|
static let destructive = Color.red
|
|
}
|
|
|
|
// Apply tint to views
|
|
ContentView()
|
|
.tint(AppColors.primary)
|
|
```
|
|
|
|
## Navigation Patterns
|
|
|
|
### Hierarchical Navigation
|
|
|
|
```swift
|
|
struct MasterDetailView: View {
|
|
@State private var selectedItem: Item?
|
|
@Environment(\.horizontalSizeClass) private var sizeClass
|
|
|
|
var body: some View {
|
|
NavigationSplitView {
|
|
// Sidebar
|
|
List(items, selection: $selectedItem) { item in
|
|
NavigationLink(value: item) {
|
|
ItemRow(item: item)
|
|
}
|
|
}
|
|
.navigationTitle("Items")
|
|
.toolbar {
|
|
ToolbarItem(placement: .primaryAction) {
|
|
Button("Add", systemImage: "plus") { }
|
|
}
|
|
}
|
|
} detail: {
|
|
// Detail view
|
|
if let item = selectedItem {
|
|
ItemDetailView(item: item)
|
|
} else {
|
|
ContentUnavailableView(
|
|
"Select an Item",
|
|
systemImage: "sidebar.leading"
|
|
)
|
|
}
|
|
}
|
|
.navigationSplitViewStyle(.balanced)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Tab-Based Navigation
|
|
|
|
```swift
|
|
struct MainTabView: View {
|
|
@State private var selectedTab: Tab = .home
|
|
|
|
enum Tab: String, CaseIterable {
|
|
case home, explore, notifications, profile
|
|
|
|
var title: String {
|
|
rawValue.capitalized
|
|
}
|
|
|
|
var systemImage: String {
|
|
switch self {
|
|
case .home: return "house"
|
|
case .explore: return "magnifyingglass"
|
|
case .notifications: return "bell"
|
|
case .profile: return "person"
|
|
}
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
TabView(selection: $selectedTab) {
|
|
ForEach(Tab.allCases, id: \.self) { tab in
|
|
NavigationStack {
|
|
tabContent(for: tab)
|
|
}
|
|
.tabItem {
|
|
Label(tab.title, systemImage: tab.systemImage)
|
|
}
|
|
.tag(tab)
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func tabContent(for tab: Tab) -> some View {
|
|
switch tab {
|
|
case .home:
|
|
HomeView()
|
|
case .explore:
|
|
ExploreView()
|
|
case .notifications:
|
|
NotificationsView()
|
|
case .profile:
|
|
ProfileView()
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Toolbar Patterns
|
|
|
|
### Standard Toolbar Items
|
|
|
|
```swift
|
|
struct ContentView: View {
|
|
@State private var isEditing = false
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
List { /* content */ }
|
|
.navigationTitle("Items")
|
|
.toolbar {
|
|
// Leading items
|
|
ToolbarItem(placement: .topBarLeading) {
|
|
EditButton()
|
|
}
|
|
|
|
// Trailing items
|
|
ToolbarItemGroup(placement: .topBarTrailing) {
|
|
Button("Filter", systemImage: "line.3.horizontal.decrease.circle") { }
|
|
Button("Add", systemImage: "plus") { }
|
|
}
|
|
|
|
// Bottom bar
|
|
ToolbarItemGroup(placement: .bottomBar) {
|
|
Button("Archive", systemImage: "archivebox") { }
|
|
Spacer()
|
|
Text("\(itemCount) items")
|
|
.font(.footnote)
|
|
.foregroundStyle(.secondary)
|
|
Spacer()
|
|
Button("Share", systemImage: "square.and.arrow.up") { }
|
|
}
|
|
}
|
|
.toolbarBackground(.visible, for: .bottomBar)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Search Integration
|
|
|
|
```swift
|
|
struct SearchableView: View {
|
|
@State private var searchText = ""
|
|
@State private var searchScope: SearchScope = .all
|
|
@State private var isSearching = false
|
|
|
|
enum SearchScope: String, CaseIterable {
|
|
case all, titles, content
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
List(filteredItems) { item in
|
|
ItemRow(item: item)
|
|
}
|
|
.navigationTitle("Library")
|
|
.searchable(
|
|
text: $searchText,
|
|
isPresented: $isSearching,
|
|
placement: .navigationBarDrawer(displayMode: .always)
|
|
)
|
|
.searchScopes($searchScope) {
|
|
ForEach(SearchScope.allCases, id: \.self) { scope in
|
|
Text(scope.rawValue.capitalized).tag(scope)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Feedback Patterns
|
|
|
|
### Haptic Feedback
|
|
|
|
```swift
|
|
struct HapticFeedback {
|
|
static func impact(_ style: UIImpactFeedbackGenerator.FeedbackStyle = .medium) {
|
|
let generator = UIImpactFeedbackGenerator(style: style)
|
|
generator.impactOccurred()
|
|
}
|
|
|
|
static func notification(_ type: UINotificationFeedbackGenerator.FeedbackType) {
|
|
let generator = UINotificationFeedbackGenerator()
|
|
generator.notificationOccurred(type)
|
|
}
|
|
|
|
static func selection() {
|
|
let generator = UISelectionFeedbackGenerator()
|
|
generator.selectionChanged()
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
Button("Submit") {
|
|
HapticFeedback.notification(.success)
|
|
submit()
|
|
}
|
|
```
|
|
|
|
### Visual Feedback
|
|
|
|
```swift
|
|
struct FeedbackButton: View {
|
|
let title: String
|
|
let action: () -> Void
|
|
|
|
@State private var showSuccess = false
|
|
|
|
var body: some View {
|
|
Button(title) {
|
|
action()
|
|
withAnimation {
|
|
showSuccess = true
|
|
}
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
|
withAnimation {
|
|
showSuccess = false
|
|
}
|
|
}
|
|
}
|
|
.overlay(alignment: .trailing) {
|
|
if showSuccess {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.foregroundStyle(.green)
|
|
.transition(.scale.combined(with: .opacity))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Accessibility
|
|
|
|
### VoiceOver Support
|
|
|
|
```swift
|
|
struct AccessibleCard: View {
|
|
let item: Item
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text(item.title)
|
|
.font(.headline)
|
|
Text(item.subtitle)
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
|
|
HStack {
|
|
Image(systemName: "star.fill")
|
|
Text("\(item.rating, specifier: "%.1f")")
|
|
}
|
|
}
|
|
.padding()
|
|
.accessibilityElement(children: .combine)
|
|
.accessibilityLabel("\(item.title), \(item.subtitle)")
|
|
.accessibilityValue("Rating: \(item.rating) stars")
|
|
.accessibilityHint("Double tap to view details")
|
|
.accessibilityAddTraits(.isButton)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Dynamic Type Support
|
|
|
|
```swift
|
|
struct DynamicTypeView: View {
|
|
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
|
|
|
|
var body: some View {
|
|
Group {
|
|
if dynamicTypeSize.isAccessibilitySize {
|
|
// Stack vertically for accessibility sizes
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
leadingContent
|
|
trailingContent
|
|
}
|
|
} else {
|
|
// Side-by-side for standard sizes
|
|
HStack {
|
|
leadingContent
|
|
Spacer()
|
|
trailingContent
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var leadingContent: some View {
|
|
Label("Items", systemImage: "folder")
|
|
}
|
|
|
|
var trailingContent: some View {
|
|
Text("12")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Error Handling UI
|
|
|
|
### Error States
|
|
|
|
```swift
|
|
struct ErrorView: View {
|
|
let error: Error
|
|
let retryAction: () async -> Void
|
|
|
|
var body: some View {
|
|
ContentUnavailableView {
|
|
Label("Unable to Load", systemImage: "exclamationmark.triangle")
|
|
} description: {
|
|
Text(error.localizedDescription)
|
|
} actions: {
|
|
Button("Try Again") {
|
|
Task {
|
|
await retryAction()
|
|
}
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Empty States
|
|
|
|
```swift
|
|
struct EmptyStateView: View {
|
|
let title: String
|
|
let description: String
|
|
let systemImage: String
|
|
let action: (() -> Void)?
|
|
let actionTitle: String?
|
|
|
|
var body: some View {
|
|
ContentUnavailableView {
|
|
Label(title, systemImage: systemImage)
|
|
} description: {
|
|
Text(description)
|
|
} actions: {
|
|
if let action, let actionTitle {
|
|
Button(actionTitle, action: action)
|
|
.buttonStyle(.borderedProminent)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
EmptyStateView(
|
|
title: "No Photos",
|
|
description: "Take your first photo to get started.",
|
|
systemImage: "camera",
|
|
action: { showCamera = true },
|
|
actionTitle: "Take Photo"
|
|
)
|
|
```
|