Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Design iOS mobile app UI/UX following Apple Human Interface Guidelines and SwiftUI/UIKit patterns.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/hig-patterns.md
1# iOS Human Interface Guidelines Patterns23## Layout and Spacing45### Standard Margins and Padding67```swift8// System standard margins9private let standardMargin: CGFloat = 1610private let compactMargin: CGFloat = 811private let largeMargin: CGFloat = 241213// Content insets following HIG14extension EdgeInsets {15static let standard = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)16static let listRow = EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)17static let card = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)18}19```2021### Safe Area Handling2223```swift24struct SafeAreaAwareView: View {25var body: some View {26ScrollView {27LazyVStack(spacing: 16) {28ForEach(items) { item in29ItemRow(item: item)30}31}32.padding(.horizontal)33}34.safeAreaInset(edge: .bottom) {35// Floating action area36HStack {37Button("Cancel") { }38.buttonStyle(.bordered)39Spacer()40Button("Confirm") { }41.buttonStyle(.borderedProminent)42}43.padding()44.background(.regularMaterial)45}46}47}48```4950### Adaptive Layouts5152```swift53struct AdaptiveGridView: View {54@Environment(\.horizontalSizeClass) private var sizeClass5556private var columns: [GridItem] {57switch sizeClass {58case .compact:59return [GridItem(.flexible())]60case .regular:61return [62GridItem(.flexible()),63GridItem(.flexible()),64GridItem(.flexible())65]66default:67return [GridItem(.flexible())]68}69}7071var body: some View {72ScrollView {73LazyVGrid(columns: columns, spacing: 16) {74ForEach(items) { item in75ItemCard(item: item)76}77}78.padding()79}80}81}82```8384## Typography Hierarchy8586### System Font Styles8788```swift89// HIG-compliant typography scale90struct Typography {91// Titles92static let largeTitle = Font.largeTitle.weight(.bold) // 34pt bold93static let title = Font.title.weight(.semibold) // 28pt semibold94static let title2 = Font.title2.weight(.semibold) // 22pt semibold95static let title3 = Font.title3.weight(.semibold) // 20pt semibold9697// Headlines and body98static let headline = Font.headline // 17pt semibold99static let body = Font.body // 17pt regular100static let callout = Font.callout // 16pt regular101102// Supporting text103static let subheadline = Font.subheadline // 15pt regular104static let footnote = Font.footnote // 13pt regular105static let caption = Font.caption // 12pt regular106static let caption2 = Font.caption2 // 11pt regular107}108```109110### Custom Font with Dynamic Type111112```swift113extension Font {114static func customBody(_ name: String) -> Font {115.custom(name, size: 17, relativeTo: .body)116}117118static func customHeadline(_ name: String) -> Font {119.custom(name, size: 17, relativeTo: .headline)120.weight(.semibold)121}122}123124// Usage125Text("Custom styled text")126.font(.customBody("Avenir Next"))127```128129## Color System130131### Semantic Colors132133```swift134// Use semantic colors for automatic light/dark mode support135extension Color {136// Labels137static let primaryLabel = Color.primary138static let secondaryLabel = Color.secondary139static let tertiaryLabel = Color(uiColor: .tertiaryLabel)140141// Backgrounds142static let systemBackground = Color(uiColor: .systemBackground)143static let secondaryBackground = Color(uiColor: .secondarySystemBackground)144static let groupedBackground = Color(uiColor: .systemGroupedBackground)145146// Fills147static let primaryFill = Color(uiColor: .systemFill)148static let secondaryFill = Color(uiColor: .secondarySystemFill)149150// Separators151static let separator = Color(uiColor: .separator)152static let opaqueSeparator = Color(uiColor: .opaqueSeparator)153}154```155156### Tint Colors157158```swift159// App-wide tint color160struct AppColors {161static let primary = Color.blue162static let secondary = Color.purple163static let success = Color.green164static let warning = Color.orange165static let error = Color.red166167// Semantic tints168static let interactive = Color.accentColor169static let destructive = Color.red170}171172// Apply tint to views173ContentView()174.tint(AppColors.primary)175```176177## Navigation Patterns178179### Hierarchical Navigation180181```swift182struct MasterDetailView: View {183@State private var selectedItem: Item?184@Environment(\.horizontalSizeClass) private var sizeClass185186var body: some View {187NavigationSplitView {188// Sidebar189List(items, selection: $selectedItem) { item in190NavigationLink(value: item) {191ItemRow(item: item)192}193}194.navigationTitle("Items")195.toolbar {196ToolbarItem(placement: .primaryAction) {197Button("Add", systemImage: "plus") { }198}199}200} detail: {201// Detail view202if let item = selectedItem {203ItemDetailView(item: item)204} else {205ContentUnavailableView(206"Select an Item",207systemImage: "sidebar.leading"208)209}210}211.navigationSplitViewStyle(.balanced)212}213}214```215216### Tab-Based Navigation217218```swift219struct MainTabView: View {220@State private var selectedTab: Tab = .home221222enum Tab: String, CaseIterable {223case home, explore, notifications, profile224225var title: String {226rawValue.capitalized227}228229var systemImage: String {230switch self {231case .home: return "house"232case .explore: return "magnifyingglass"233case .notifications: return "bell"234case .profile: return "person"235}236}237}238239var body: some View {240TabView(selection: $selectedTab) {241ForEach(Tab.allCases, id: \.self) { tab in242NavigationStack {243tabContent(for: tab)244}245.tabItem {246Label(tab.title, systemImage: tab.systemImage)247}248.tag(tab)249}250}251}252253@ViewBuilder254private func tabContent(for tab: Tab) -> some View {255switch tab {256case .home:257HomeView()258case .explore:259ExploreView()260case .notifications:261NotificationsView()262case .profile:263ProfileView()264}265}266}267```268269## Toolbar Patterns270271### Standard Toolbar Items272273```swift274struct ContentView: View {275@State private var isEditing = false276277var body: some View {278NavigationStack {279List { /* content */ }280.navigationTitle("Items")281.toolbar {282// Leading items283ToolbarItem(placement: .topBarLeading) {284EditButton()285}286287// Trailing items288ToolbarItemGroup(placement: .topBarTrailing) {289Button("Filter", systemImage: "line.3.horizontal.decrease.circle") { }290Button("Add", systemImage: "plus") { }291}292293// Bottom bar294ToolbarItemGroup(placement: .bottomBar) {295Button("Archive", systemImage: "archivebox") { }296Spacer()297Text("\(itemCount) items")298.font(.footnote)299.foregroundStyle(.secondary)300Spacer()301Button("Share", systemImage: "square.and.arrow.up") { }302}303}304.toolbarBackground(.visible, for: .bottomBar)305}306}307}308```309310### Search Integration311312```swift313struct SearchableView: View {314@State private var searchText = ""315@State private var searchScope: SearchScope = .all316@State private var isSearching = false317318enum SearchScope: String, CaseIterable {319case all, titles, content320}321322var body: some View {323NavigationStack {324List(filteredItems) { item in325ItemRow(item: item)326}327.navigationTitle("Library")328.searchable(329text: $searchText,330isPresented: $isSearching,331placement: .navigationBarDrawer(displayMode: .always)332)333.searchScopes($searchScope) {334ForEach(SearchScope.allCases, id: \.self) { scope in335Text(scope.rawValue.capitalized).tag(scope)336}337}338}339}340}341```342343## Feedback Patterns344345### Haptic Feedback346347```swift348struct HapticFeedback {349static func impact(_ style: UIImpactFeedbackGenerator.FeedbackStyle = .medium) {350let generator = UIImpactFeedbackGenerator(style: style)351generator.impactOccurred()352}353354static func notification(_ type: UINotificationFeedbackGenerator.FeedbackType) {355let generator = UINotificationFeedbackGenerator()356generator.notificationOccurred(type)357}358359static func selection() {360let generator = UISelectionFeedbackGenerator()361generator.selectionChanged()362}363}364365// Usage366Button("Submit") {367HapticFeedback.notification(.success)368submit()369}370```371372### Visual Feedback373374```swift375struct FeedbackButton: View {376let title: String377let action: () -> Void378379@State private var showSuccess = false380381var body: some View {382Button(title) {383action()384withAnimation {385showSuccess = true386}387DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {388withAnimation {389showSuccess = false390}391}392}393.overlay(alignment: .trailing) {394if showSuccess {395Image(systemName: "checkmark.circle.fill")396.foregroundStyle(.green)397.transition(.scale.combined(with: .opacity))398}399}400}401}402```403404## Accessibility405406### VoiceOver Support407408```swift409struct AccessibleCard: View {410let item: Item411412var body: some View {413VStack(alignment: .leading, spacing: 8) {414Text(item.title)415.font(.headline)416Text(item.subtitle)417.font(.subheadline)418.foregroundStyle(.secondary)419420HStack {421Image(systemName: "star.fill")422Text("\(item.rating, specifier: "%.1f")")423}424}425.padding()426.accessibilityElement(children: .combine)427.accessibilityLabel("\(item.title), \(item.subtitle)")428.accessibilityValue("Rating: \(item.rating) stars")429.accessibilityHint("Double tap to view details")430.accessibilityAddTraits(.isButton)431}432}433```434435### Dynamic Type Support436437```swift438struct DynamicTypeView: View {439@Environment(\.dynamicTypeSize) private var dynamicTypeSize440441var body: some View {442Group {443if dynamicTypeSize.isAccessibilitySize {444// Stack vertically for accessibility sizes445VStack(alignment: .leading, spacing: 12) {446leadingContent447trailingContent448}449} else {450// Side-by-side for standard sizes451HStack {452leadingContent453Spacer()454trailingContent455}456}457}458}459460var leadingContent: some View {461Label("Items", systemImage: "folder")462}463464var trailingContent: some View {465Text("12")466.foregroundStyle(.secondary)467}468}469```470471## Error Handling UI472473### Error States474475```swift476struct ErrorView: View {477let error: Error478let retryAction: () async -> Void479480var body: some View {481ContentUnavailableView {482Label("Unable to Load", systemImage: "exclamationmark.triangle")483} description: {484Text(error.localizedDescription)485} actions: {486Button("Try Again") {487Task {488await retryAction()489}490}491.buttonStyle(.borderedProminent)492}493}494}495```496497### Empty States498499```swift500struct EmptyStateView: View {501let title: String502let description: String503let systemImage: String504let action: (() -> Void)?505let actionTitle: String?506507var body: some View {508ContentUnavailableView {509Label(title, systemImage: systemImage)510} description: {511Text(description)512} actions: {513if let action, let actionTitle {514Button(actionTitle, action: action)515.buttonStyle(.borderedProminent)516}517}518}519}520521// Usage522EmptyStateView(523title: "No Photos",524description: "Take your first photo to get started.",525systemImage: "camera",526action: { showCamera = true },527actionTitle: "Take Photo"528)529```530