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/swiftui-components.md
1# SwiftUI Component Library23## Lists and Collections45### Basic List67```swift8struct ItemListView: View {9@State private var items: [Item] = []1011var body: some View {12List {13ForEach(items) { item in14ItemRow(item: item)15}16.onDelete(perform: deleteItems)17.onMove(perform: moveItems)18}19.listStyle(.insetGrouped)20.refreshable {21await loadItems()22}23}2425private func deleteItems(at offsets: IndexSet) {26items.remove(atOffsets: offsets)27}2829private func moveItems(from source: IndexSet, to destination: Int) {30items.move(fromOffsets: source, toOffset: destination)31}32}33```3435### Sectioned List3637```swift38struct SectionedListView: View {39let groupedItems: [String: [Item]]4041var body: some View {42List {43ForEach(groupedItems.keys.sorted(), id: \.self) { key in44Section(header: Text(key)) {45ForEach(groupedItems[key] ?? []) { item in46ItemRow(item: item)47}48}49}50}51.listStyle(.sidebar)52}53}54```5556### Search Integration5758```swift59struct SearchableListView: View {60@State private var searchText = ""61@State private var items: [Item] = []6263var filteredItems: [Item] {64if searchText.isEmpty {65return items66}67return items.filter { $0.name.localizedCaseInsensitiveContains(searchText) }68}6970var body: some View {71NavigationStack {72List(filteredItems) { item in73ItemRow(item: item)74}75.searchable(text: $searchText, prompt: "Search items")76.searchSuggestions {77ForEach(searchSuggestions, id: \.self) { suggestion in78Text(suggestion)79.searchCompletion(suggestion)80}81}82.navigationTitle("Items")83}84}85}86```8788## Forms and Input8990### Settings Form9192```swift93struct SettingsView: View {94@AppStorage("notifications") private var notificationsEnabled = true95@AppStorage("soundEnabled") private var soundEnabled = true96@State private var selectedTheme = Theme.system97@State private var username = ""9899var body: some View {100Form {101Section("Account") {102TextField("Username", text: $username)103.textContentType(.username)104.autocorrectionDisabled()105}106107Section("Preferences") {108Toggle("Enable Notifications", isOn: $notificationsEnabled)109Toggle("Sound Effects", isOn: $soundEnabled)110111Picker("Theme", selection: $selectedTheme) {112ForEach(Theme.allCases) { theme in113Text(theme.rawValue).tag(theme)114}115}116}117118Section("About") {119LabeledContent("Version", value: "1.0.0")120121Link(destination: URL(string: "https://example.com/privacy")!) {122Text("Privacy Policy")123}124}125}126.navigationTitle("Settings")127}128}129```130131### Custom Input Fields132133```swift134struct ValidatedTextField: View {135let title: String136@Binding var text: String137let validation: (String) -> Bool138139@State private var isValid = true140@FocusState private var isFocused: Bool141142var body: some View {143VStack(alignment: .leading, spacing: 4) {144Text(title)145.font(.caption)146.foregroundStyle(.secondary)147148TextField(title, text: $text)149.textFieldStyle(.roundedBorder)150.focused($isFocused)151.overlay(152RoundedRectangle(cornerRadius: 8)153.stroke(borderColor, lineWidth: 1)154)155.onChange(of: text) { _, newValue in156isValid = validation(newValue)157}158159if !isValid && !text.isEmpty {160Text("Invalid input")161.font(.caption)162.foregroundStyle(.red)163}164}165}166167private var borderColor: Color {168if isFocused {169return isValid ? .blue : .red170}171return .clear172}173}174```175176## Buttons and Actions177178### Button Styles179180```swift181// Primary filled button182Button("Continue") {183// action184}185.buttonStyle(.borderedProminent)186.controlSize(.large)187188// Secondary button189Button("Cancel") {190// action191}192.buttonStyle(.bordered)193194// Destructive button195Button("Delete", role: .destructive) {196// action197}198.buttonStyle(.bordered)199200// Custom button style201struct ScaleButtonStyle: ButtonStyle {202func makeBody(configuration: Configuration) -> some View {203configuration.label204.scaleEffect(configuration.isPressed ? 0.95 : 1.0)205.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)206}207}208```209210### Menu and Context Menu211212```swift213// Menu button214Menu {215Button("Edit", systemImage: "pencil") { }216Button("Duplicate", systemImage: "doc.on.doc") { }217Divider()218Button("Delete", systemImage: "trash", role: .destructive) { }219} label: {220Image(systemName: "ellipsis.circle")221}222223// Context menu on any view224Text("Long press me")225.contextMenu {226Button("Copy", systemImage: "doc.on.doc") { }227Button("Share", systemImage: "square.and.arrow.up") { }228} preview: {229ItemPreviewView()230}231```232233## Sheets and Modals234235### Sheet Presentation236237```swift238struct ParentView: View {239@State private var showSettings = false240@State private var selectedItem: Item?241242var body: some View {243VStack {244Button("Settings") {245showSettings = true246}247}248.sheet(isPresented: $showSettings) {249SettingsSheet()250.presentationDetents([.medium, .large])251.presentationDragIndicator(.visible)252}253.sheet(item: $selectedItem) { item in254ItemDetailSheet(item: item)255.presentationDetents([.height(300), .large])256.presentationCornerRadius(24)257}258}259}260261struct SettingsSheet: View {262@Environment(\.dismiss) private var dismiss263264var body: some View {265NavigationStack {266SettingsContent()267.toolbar {268ToolbarItem(placement: .confirmationAction) {269Button("Done") {270dismiss()271}272}273}274}275}276}277```278279### Confirmation Dialog280281```swift282struct DeleteConfirmationView: View {283@State private var showConfirmation = false284285var body: some View {286Button("Delete Account", role: .destructive) {287showConfirmation = true288}289.confirmationDialog(290"Delete Account",291isPresented: $showConfirmation,292titleVisibility: .visible293) {294Button("Delete", role: .destructive) {295deleteAccount()296}297Button("Cancel", role: .cancel) { }298} message: {299Text("This action cannot be undone.")300}301}302}303```304305## Loading and Progress306307### Progress Indicators308309```swift310// Indeterminate spinner311ProgressView()312.progressViewStyle(.circular)313314// Determinate progress315ProgressView(value: downloadProgress, total: 1.0) {316Text("Downloading...")317} currentValueLabel: {318Text("\(Int(downloadProgress * 100))%")319}320321// Custom loading view322struct LoadingOverlay: View {323let message: String324325var body: some View {326ZStack {327Color.black.opacity(0.4)328.ignoresSafeArea()329330VStack(spacing: 16) {331ProgressView()332.scaleEffect(1.5)333.tint(.white)334335Text(message)336.font(.subheadline)337.foregroundStyle(.white)338}339.padding(24)340.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16))341}342}343}344```345346### Skeleton Loading347348```swift349struct SkeletonRow: View {350@State private var isAnimating = false351352var body: some View {353HStack(spacing: 12) {354Circle()355.fill(.gray.opacity(0.3))356.frame(width: 44, height: 44)357358VStack(alignment: .leading, spacing: 8) {359RoundedRectangle(cornerRadius: 4)360.fill(.gray.opacity(0.3))361.frame(height: 14)362.frame(maxWidth: 200)363364RoundedRectangle(cornerRadius: 4)365.fill(.gray.opacity(0.2))366.frame(height: 12)367.frame(maxWidth: 150)368}369}370.opacity(isAnimating ? 0.5 : 1.0)371.animation(.easeInOut(duration: 0.8).repeatForever(), value: isAnimating)372.onAppear { isAnimating = true }373}374}375```376377## Async Content Loading378379### AsyncImage380381```swift382AsyncImage(url: imageURL) { phase in383switch phase {384case .empty:385ProgressView()386case .success(let image):387image388.resizable()389.aspectRatio(contentMode: .fill)390case .failure:391Image(systemName: "photo")392.foregroundStyle(.secondary)393@unknown default:394EmptyView()395}396}397.frame(width: 100, height: 100)398.clipShape(RoundedRectangle(cornerRadius: 8))399```400401### Task-Based Loading402403```swift404struct AsyncContentView: View {405@State private var items: [Item] = []406@State private var isLoading = true407@State private var error: Error?408409var body: some View {410Group {411if isLoading {412ProgressView("Loading...")413} else if let error {414ContentUnavailableView(415"Failed to Load",416systemImage: "exclamationmark.triangle",417description: Text(error.localizedDescription)418)419} else if items.isEmpty {420ContentUnavailableView(421"No Items",422systemImage: "tray",423description: Text("Add your first item to get started.")424)425} else {426List(items) { item in427ItemRow(item: item)428}429}430}431.task {432await loadItems()433}434}435436private func loadItems() async {437do {438items = try await api.fetchItems()439isLoading = false440} catch {441self.error = error442isLoading = false443}444}445}446```447448## Animations449450### Implicit Animations451452```swift453struct AnimatedCard: View {454@State private var isExpanded = false455456var body: some View {457VStack {458Text("Tap to expand")459460if isExpanded {461Text("Additional content here")462.transition(.move(edge: .top).combined(with: .opacity))463}464}465.padding()466.frame(maxWidth: .infinity)467.background(.blue.opacity(0.1), in: RoundedRectangle(cornerRadius: 12))468.onTapGesture {469withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {470isExpanded.toggle()471}472}473}474}475```476477### Custom Transitions478479```swift480extension AnyTransition {481static var slideAndFade: AnyTransition {482.asymmetric(483insertion: .move(edge: .trailing).combined(with: .opacity),484removal: .move(edge: .leading).combined(with: .opacity)485)486}487488static var scaleAndFade: AnyTransition {489.scale(scale: 0.8).combined(with: .opacity)490}491}492```493494### Phase Animator (iOS 17+)495496```swift497struct PulsingButton: View {498var body: some View {499Button("Tap Me") { }500.buttonStyle(.borderedProminent)501.phaseAnimator([false, true]) { content, phase in502content503.scaleEffect(phase ? 1.05 : 1.0)504} animation: { _ in505.easeInOut(duration: 0.5)506}507}508}509```510511## Gestures512513### Drag Gesture514515```swift516struct DraggableCard: View {517@State private var offset = CGSize.zero518@State private var isDragging = false519520var body: some View {521RoundedRectangle(cornerRadius: 16)522.fill(.blue)523.frame(width: 200, height: 150)524.offset(offset)525.scaleEffect(isDragging ? 1.05 : 1.0)526.gesture(527DragGesture()528.onChanged { value in529offset = value.translation530isDragging = true531}532.onEnded { _ in533withAnimation(.spring()) {534offset = .zero535isDragging = false536}537}538)539}540}541```542543### Simultaneous Gestures544545```swift546struct ZoomableImage: View {547@State private var scale: CGFloat = 1.0548@State private var lastScale: CGFloat = 1.0549550var body: some View {551Image("photo")552.resizable()553.aspectRatio(contentMode: .fit)554.scaleEffect(scale)555.gesture(556MagnificationGesture()557.onChanged { value in558scale = lastScale * value559}560.onEnded { _ in561lastScale = scale562}563)564.gesture(565TapGesture(count: 2)566.onEnded {567withAnimation {568scale = 1.0569lastScale = 1.0570}571}572)573}574}575```576