Mobile iOS Design
Seth HobsonCommunityCreated by Seth Hobson
- Skill
- Tool integration
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a comprehensive framework for developing native iOS applications that adhere to Apple's Human Interface Guidelines (HIG). It covers essential SwiftUI layout systems, navigation patterns, and system integration techniques. By focusing on semantic design, accessibility, and platform-specific behaviors, it helps developers create interfaces that feel natural and performant across iPhone, iPad, and other Apple devices.
What it does
- Provides patterns for implementing adaptive layouts using stacks and grids.
- Explains navigation structures like NavigationStack and NavigationSplitView.
- Offers guidance on using semantic colors and materials for light and dark mode support.
- Details the integration of SF Symbols and Dynamic Type for accessible typography.
- Outlines best practices for state restoration and iPad multitasking.
- Includes code snippets for common UI components and system-compliant design.
When to use it
- When designing or building user interfaces for native iOS applications.
- When ensuring an app adheres to Apple's design principles and accessibility standards.
- When implementing complex navigation flows or adaptive layouts for multiple screen sizes.
- When troubleshooting layout, performance, or accessibility issues in SwiftUI views.
Advantages
- Ensures consistency with Apple's platform design language.
- Promotes the creation of accessible and inclusive user experiences.
- Reduces development time with reusable, HIG-compliant code patterns.
- Facilitates seamless support for system features like Dark Mode and Dynamic Type.
Usage examples
- “How do I create a grid layout that adapts to different screen sizes in SwiftUI?”
- The skill provides an implementation using LazyVGrid with GridItem.adaptive, demonstrating how to switch column counts based on the horizontalSizeClass environment value.
- “What is the best way to handle navigation in an iOS app using SwiftUI?”
- It explains the use of NavigationStack for hierarchical navigation and provides examples for programmatic navigation and state persistence using @SceneStorage.
- “How can I ensure my app supports both light and dark mode automatically?”
- The skill instructs on using semantic colors like .primary, .secondary, and .background, which automatically adapt to the system's appearance settings.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/wshobson/mobile-ios-design/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip mobile-ios-design.zip -d .agents/skills
Documentation by Seth Hobson
iOS Mobile Design
Master iOS Human Interface Guidelines (HIG) and SwiftUI patterns to build polished, native iOS applications that feel at home on Apple platforms.
When to Use This Skill
Designing iOS app interfaces following Apple HIG
Building SwiftUI views and layouts
Implementing iOS navigation patterns (NavigationStack, TabView, sheets)
Creating adaptive layouts for iPhone and iPad
Using SF Symbols and system typography
Building accessible iOS interfaces
Implementing iOS-specific gestures and interactions
Designing for Dynamic Type and Dark Mode
Core Concepts
1. Human Interface Guidelines Principles
Clarity: Content is legible, icons are precise, adornments are subtle Deference: UI helps users understand content without competing with it Depth: Visual layers and motion convey hierarchy and enable navigation
Platform Considerations:
iOS: Touch-first, compact displays, portrait orientation
iPadOS: Larger canvas, multitasking, pointer support
visionOS: Spatial computing, eye/hand input
2. SwiftUI Layout System
Stack-Based Layouts:
// Vertical stack with alignment
VStack(alignment: .leading, spacing: 12) {
Text("Title")
.font(.headline)
Text("Subtitle")
.font(.subheadline)
.foregroundStyle(.secondary)
}
// Horizontal stack with flexible spacing
HStack {
Image(systemName: "star.fill")
Text("Featured")
Spacer()
Text("View All")
.foregroundStyle(.blue)
}Grid Layouts:
// Adaptive grid that fills available width
LazyVGrid(columns: [
GridItem(.adaptive(minimum: 150, maximum: 200))
], spacing: 16) {
ForEach(items) { item in
ItemCard(item: item)
}
}
// Fixed column grid
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
], spacing: 12) {
ForEach(items) { item in
ItemThumbnail(item: item)
}
}3. Navigation Patterns
NavigationStack (iOS 16+):
struct ContentView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
List(items) { item in
NavigationLink(value: item) {
ItemRow(item: item)
}
}
.navigationTitle("Items")
.navigationDestination(for: Item.self) { item in
ItemDetailView(item: item)
}
}
}
}TabView (iOS 18+):
struct MainTabView: View {
@State private var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab) {
Tab("Home", systemImage: "house", value: 0) {
HomeView()
}
Tab("Search", systemImage: "magnifyingglass", value: 1) {
SearchView()
}
Tab("Profile", systemImage: "person", value: 2) {
ProfileView()
}
}
}
}4. System Integration
SF Symbols:
// Basic symbol
Image(systemName: "heart.fill")
.foregroundStyle(.red)
// Symbol with rendering mode
Image(systemName: "cloud.sun.fill")
.symbolRenderingMode(.multicolor)
// Variable symbol (iOS 16+)
Image(systemName: "speaker.wave.3.fill", variableValue: volume)
// Symbol effect (iOS 17+)
Image(systemName: "bell.fill")
.symbolEffect(.bounce, value: notificationCount)Dynamic Type:
// Use semantic fonts
Text("Headline")
.font(.headline)
Text("Body text that scales with user preferences")
.font(.body)
// Custom font that respects Dynamic Type
Text("Custom")
.font(.custom("Avenir", size: 17, relativeTo: .body))5. Visual Design
Colors and Materials:
// Semantic colors that adapt to light/dark mode
Text("Primary")
.foregroundStyle(.primary)
Text("Secondary")
.foregroundStyle(.secondary)
// System materials for blur effects
Rectangle()
.fill(.ultraThinMaterial)
.frame(height: 100)
// Vibrant materials for overlays
Text("Overlay")
.padding()
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12))Shadows and Depth:
// Standard card shadow
RoundedRectangle(cornerRadius: 16)
.fill(.background)
.shadow(color: .black.opacity(0.1), radius: 8, y: 4)
// Elevated appearance
.shadow(radius: 2, y: 1)
.shadow(radius: 8, y: 4)Quick Start Component
import SwiftUI
struct FeatureCard: View {
let title: String
let description: String
let systemImage: String
var body: some View {
HStack(spacing: 16) {
Image(systemName: systemImage)
.font(.title)
.foregroundStyle(.blue)
.frame(width: 44, height: 44)
.background(.blue.opacity(0.1), in: Circle())
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.headline)
Text(description)
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(2)
}
Spacer()
Image(systemName: "chevron.right")
.foregroundStyle(.tertiary)
}
.padding()
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.shadow(color: .black.opacity(0.05), radius: 4, y: 2)
}
}Best Practices
Use Semantic Colors: Always use
.primary,.secondary,.backgroundfor automatic light/dark mode supportEmbrace SF Symbols: Use system symbols for consistency and automatic accessibility
Support Dynamic Type: Use semantic fonts (
.body,.headline) instead of fixed sizesAdd Accessibility: Include
.accessibilityLabel()and.accessibilityHint()modifiersUse Safe Areas: Respect
safeAreaInsetand avoid hardcoded padding at screen edgesImplement State Restoration: Use
@SceneStoragefor preserving user stateSupport iPad Multitasking: Design for split view and slide over
Test on Device: Simulator doesn't capture full haptic and performance experience
Common Issues
Layout Breaking: Use
.fixedSize()sparingly; prefer flexible layoutsPerformance Issues: Use
LazyVStack/LazyHStackfor long scrolling listsNavigation Bugs: Ensure
NavigationLinkvalues areHashableDark Mode Problems: Avoid hardcoded colors; use semantic or asset catalog colors
Accessibility Failures: Test with VoiceOver enabled
Memory Leaks: Watch for strong reference cycles in closures
Files
- ATTRIBUTION.mdAdded by AutoGineer969 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md6.8 KiB
- references/hig-patterns.md13.3 KiB
- references/ios-navigation.md14.4 KiB
- references/swiftui-components.md13.8 KiB
references/hig-patterns.md
iOS Human Interface Guidelines Patterns
Layout and Spacing
Standard Margins and Padding
// 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
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
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
// 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
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
// 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
// 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
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
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
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
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
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
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
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
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
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
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"
)Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Seth Hobson
- Publisher
- Seth Hobson (Community)
- Source
- wshobson/agents · plugins/ui-design/skills/mobile-ios-design · commit 4bde316
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Seth Hobson or Seth Hobson.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download mobile-ios-design.zip