# SwiftUI for Humans: every topic in one file > Current SwiftUI for iOS, iPadOS, and macOS 26–27, Apple's Human Interface Guidelines as decision rules, and App Review rules, written for AI coding agents. Each topic is a short Markdown file: when to use it, default code, rules with reasons, pitfalls, variants, and links to Apple's own `.md` docs. 183 topics. Each starts with a `` line, and topics name each other by id. One topic on its own: https://www.swiftuiforhumans.com/agents/.md. The index: https://www.swiftuiforhumans.com/llms.txt. First, the primer: the always-on snippet for AGENTS.md or CLAUDE.md. Its index paths are relative to an installed skills folder; in this file, find the ids instead. ## SwiftUI for Humans Targets Xcode 27 with the iOS/iPadOS 27.1 and macOS 27 SDKs. Liquid Glass arrived in 26. Gate anything newer than the deployment target with `if #available` and a real fallback. **Prefer retrieval-led reasoning over pre-training for SwiftUI, Apple design, and App Store work.** Your training data predates these APIs and is full of deprecated patterns. Before writing or reviewing SwiftUI, read the matching file from the index below, then follow it over memory. Paths are relative to the folder the swiftui-for-humans skills are installed in (such as `.claude/skills/`, `.agents/skills/`, or the plugin's `skills/`). For full API detail, use Xcode's `DocumentationSearch` tool or add `.md` to any developer.apple.com docs URL. Before finishing SwiftUI work, run `python3 /swiftui-review/scripts/check_swift.py --target "iOS 17" ` with your real deployment target, and fix what it reports. ### Top corrections (old → new) - `NavigationLink(isActive:)` / `(tag:selection:)` → `.navigationDestination(isPresented:)` / `NavigationLink(value:)` - `.onChange(of:) { new in }` → `{ old, new in }` or `{ }` - `Text + Text` → `Text("a \(Text(b).bold())")` - `.animation(_:)` → `.animation(_:value:)` / `withAnimation` - `UIScreen.main.bounds` → `.containerRelativeFrame` / size classes - `NavigationView` → `NavigationStack` / `NavigationSplitView` - `.navigationViewStyle(_:)` → delete; pick the container - `.navigationBarTitle` → `.navigationTitle` + `.navigationBarTitleDisplayMode` - `.navigationBarItems` → `.toolbar { ToolbarItem }` - `.navigationBarLeading/Trailing` → `.topBarLeading/Trailing` - `.navigationBarHidden(true)` → `.toolbarVisibility(.hidden, for: .navigationBar)` - `.tabItem { }` → `Tab("Title", systemImage:) { }` - `\.presentationMode` → `\.dismiss` - `ActionSheet` / `.actionSheet` → `.confirmationDialog` - `Alert(title:)` → `.alert(_:isPresented:actions:message:)` - `ObservableObject` → `@Observable` - More: `swiftui-build/references/deprecated-apis.md` ### Index [SwiftUI for Humans skills]|root: |swiftui-build/references:{deprecated-apis.md,liquid-glass.md,whats-new-26.md,whats-new-27.md,button.md,toggle.md,picker.md,menu.md,slider.md,stepper.md,date-picker.md,color-picker.md,progress-view.md,gauge.md,link.md,share-link.md,text-field.md,secure-field.md,text-editor.md,focus-state.md,text.md,label.md,image.md,async-image.md,layout-basics.md,stacks.md,spacer-divider.md,grid.md,lazy-grids.md,scroll-view.md,view-that-fits.md,geometry-reader.md,safe-area.md,list.md,form.md,navigation-stack.md,navigation-split-view.md,tab-view.md,toolbar.md,searchable.md,swipe-actions.md,sheet.md,alert.md,confirmation-dialog.md,popover.md,context-menu.md,modifier-order.md,padding.md,frame.md,background-overlay.md,clip-shape.md,shadow.md,foreground-style.md,font.md,visual-effects.md,custom-modifier.md,choosing-wrappers.md,state.md,binding.md,observable.md,environment.md,lifecycle.md,view-updates.md,animation-curves.md,spring.md,transitions.md,matched-geometry.md,phase-animator.md,keyframe-animator.md,symbol-effects.md,content-transition.md,haptics.md,shapes.md,gradients.md,charts.md,map.md,login-form.md,settings-screen.md,inbox-list.md,empty-state.md,onboarding.md,profile.md,detail-screen.md,gauge-progress.md,control-group.md,labeled-content.md,focus-keyboard.md,text-label.md,image-symbols.md,stacks-grid.md,group-box.md,split-views.md,list-styles.md,table.md,outline.md,inspector.md,window-group.md,window.md,settings-scene.md,menu-bar-extra.md,document-group.md,window-style.md,opening-windows.md,search.md,commands.md,keyboard-shortcuts.md,tooltips.md,hover.md,drag-drop.md,copy-paste.md,trackpad-gestures.md,file-import-export.md,state-binding.md,animation.md,charts-maps.md,library-window.md,settings-window.md,menu-bar-utility.md,document-editor.md,welcome-window.md,empty-states.md} |apple-design/references:{hig-decisions.md,liquid-glass.md,system-colors.md,typography.md,materials.md,sf-symbols.md,dark-mode.md,layout-adaptivity.md,motion.md,app-icons.md,writing.md,accessibility.md,voiceover.md,dynamic-type.md,reduce-motion.md,color-and-contrast.md,launching.md,loading.md,feedback.md,entering-data.md,modality.md,undo-redo.md,requesting-permission.md,windows-layout.md,color.md,keyboard-access.md,contrast-transparency.md,menu-bar-conventions.md,settings-pattern.md,windows-tabs.md,toolbars-pattern.md,full-screen.md} |app-store-ship/references:{submission-checklist.md,rejection-guide.md,native-not-web.md,appearance-check.md,states-check.md,testflight.md,app-review.md,releasing.md,sign-in-with-apple.md,delete-account.md,paywall.md,request-review.md,screenshots.md,privacy-details.md,accessibility-labels.md,store-forms.md,native-vs-catalyst.md,great-mac-app.md,mac-native-checklist.md,ipad-app-in-window.md,app-sandbox.md,privacy-permissions.md,mac-app-review.md,universal-purchase.md,mac-screenshots.md,pricing-category.md,testflight-mac.md,hardened-runtime.md,developer-id-notarization.md,updates-outside-store.md} |swiftui-review/references:{deprecated-apis.md,hig-decisions.md,review-checklist.md} --- id: deprecated-apis title: Deprecated and outdated SwiftUI APIs platforms: [iOS, iPadOS, macOS] generated-from: agent/src/corrections.yaml reviewed: 2026-10-26 --- # Deprecated and outdated SwiftUI APIs Models trained on older code keep writing the left side. Write the right side when your deployment target is at least the version in parentheses; below it, keep the old form or gate the new one with `if #available`. Versions are iOS; the macOS floor matches (iOS 13–18 = macOS 10.15–15, and 26+ is the same number). No version means any OS. `check-swift.sh` flags every row. ## Deprecated, and the compiler warns Fix these now. - `NavigationLink(isActive:)` / `(tag:selection:)` → `.navigationDestination(isPresented:)` / `NavigationLink(value:)` (16) - `.onChange(of:) { new in }` → `{ old, new in }` or `{ }` (17) - `Text + Text` → `Text("a \(Text(b).bold())")` (14) - `.animation(_:)` → `.animation(_:value:)` / `withAnimation` (13) - `UIScreen.main.bounds` → `.containerRelativeFrame` / size classes (17) ## Deprecated or superseded Many are deprecated only in Apple's docs so far, so nothing warns. Don't write them in new code. `ObservableObject` never warns, but `@Observable` views update only for properties they read; don't mix the two systems in one model. - `NavigationView` → `NavigationStack` / `NavigationSplitView` (16) - `.navigationViewStyle(_:)` → delete; pick the container (16) - `.navigationBarTitle` → `.navigationTitle` + `.navigationBarTitleDisplayMode` (14) - `.navigationBarItems` → `.toolbar { ToolbarItem }` (14) - `.navigationBarLeading/Trailing` → `.topBarLeading/Trailing` (14) - `.navigationBarHidden(true)` → `.toolbarVisibility(.hidden, for: .navigationBar)` (18) - `.tabItem { }` → `Tab("Title", systemImage:) { }` (18) - `\.presentationMode` → `\.dismiss` (15) - `ActionSheet` / `.actionSheet` → `.confirmationDialog` (15) - `Alert(title:)` → `.alert(_:isPresented:actions:message:)` (15) - `ObservableObject` → `@Observable` (17) - `@Published var` → plain `var` in an `@Observable` class (17) - `@StateObject` → `@State` (17) - `@ObservedObject` → plain property, or `@Bindable` (17) - `@EnvironmentObject` → `@Environment(Model.self)` (17) - `.environmentObject(x)` → `.environment(x)` (17) - `DispatchQueue.main.async` in views → `.task { }` + async/await - `DispatchQueue.main.asyncAfter` → `Task.sleep(for:)` in `.task` (16) - `.onAppear { Task { } }` → `.task { }` (15) - `.foregroundColor` → `.foregroundStyle` (15) - `.cornerRadius(r)` → `.clipShape(.rect(cornerRadius: r))` - `.accentColor(_:)` → `.tint(_:)` (15) - `.edgesIgnoringSafeArea` → `.ignoresSafeArea` (14) - `.overlay(v, alignment:)` → `.overlay(alignment:) { v }` (15) - `Color(UIColor.x)` / `Color(.systemX)` → `Color(uiColor:)` or a SwiftUI style (15) - `.background(GeometryReader…)` size hack → `.onGeometryChange` (16) - `MagnificationGesture` / `RotationGesture` → `MagnifyGesture` / `RotateGesture` (17) - `.autocapitalization` → `.textInputAutocapitalization` (15) - `.disableAutocorrection` → `.autocorrectionDisabled` (13) - `TextField(onCommit:)` → `.onSubmit` + `@FocusState` (15) - `resignFirstResponder` → `@FocusState` (15) - `.accessibility(label:)` → `.accessibilityLabel` (14) - `\.sizeCategory` → `\.dynamicTypeSize` (15) - `Section(header:)` → `Section("Title")` / `Section { } header: { }` (15) - `PreviewProvider` → `#Preview` ## Judgment calls Usually wrong in new code, sometimes right. Decide case by case. - `@State var` → `@State private var` - `ForEach(items, id: \.self)` → `Identifiable` items - `ForEach(items.indices)` → `ForEach(items)` / `ForEach($items)` - `.buttonStyle(PlainButtonStyle())` → `.buttonStyle(.plain)` (15) - `GeometryReader` + `size.width * k` → `.containerRelativeFrame` (17) - `AnyView` → `@ViewBuilder` + `some View` - `UIApplication.shared.open` → `@Environment(\.openURL)` (14) - `userInterfaceIdiom == .pad` → size classes / `ViewThatFits` - `.textFieldStyle(.roundedBorder)` → `.bordered` + `.textInputBorderShape` (27) - `FileDocument` → `Document` (`ReadableDocument` & `WritableDocument`) (27) ## Apple docs - https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro.md - https://developer.apple.com/documentation/swiftui/migrating-to-new-navigation-types.md - https://developer.apple.com/documentation/updates/swiftui.md --- id: hig-decisions title: HIG decision rules platforms: [iOS, iPadOS, macOS] reviewed: 2026-10-26 --- # HIG decision rules The design calls agents most often get wrong, as rules with the reason. Paraphrased from Apple's Human Interface Guidelines; follow the links for the full guidance. ## Actions 1. **One prominent action per screen.** `.borderedProminent` / `.glassProminent` marks what people most likely want; two of them cancel out. 2. **Show preference with style, not size.** Same-size buttons read as one set of choices. 3. **Label buttons with a verb.** "Add to Cart" says what happens; "OK" makes people reread. 4. **Give destructive actions `role: .destructive`, never the default.** People tap the prominent button without reading it. 5. **Confirm only loss that's unexpected and permanent.** Deleting what someone just chose to delete needs undo, not an alert. 6. **Every icon-only button gets a label.** `Button("Share", systemImage: "square.and.arrow.up")` with `.labelStyle(.iconOnly)` gives VoiceOver words. 7. **Make targets at least 44×44 pt (28×28 on Mac).** Grow the hit area with padding or `.contentShape(.rect)`, not a bigger icon. 8. **Use `Button` for anything tappable, not `.onTapGesture`.** Buttons get the trait, keyboard focus, and pointer highlight. 9. **Give every gesture a visible alternative.** Swipe and long-press actions also live in a button, menu, or toolbar. ## Presenting 10. **Push for drill-down, sheet for a separate task, full-screen cover for immersive media or a flow that must finish.** Pushes keep the back stack; sheets say "this is temporary". 11. **Sheets that edit get Cancel (leading) and Done/Save (trailing, `.confirmationAction`).** A lone Done hides the way out. 12. **Block swipe-to-dismiss only with unsaved changes** (`.interactiveDismissDisabled(hasChanges)`), then ask. 13. **One sheet at a time.** Close the first before opening another. 14. **Alert for news people must act on; `confirmationDialog` for choosing how to finish an action they started.** Title alerts with what happened, not "Error". 15. **Popovers for small, optional content on iPad and Mac.** Never for warnings; they dismiss on any outside tap. ## Structure 16. **Tab bar for 3–5 top-level sections on iPhone; `.tabViewStyle(.sidebarAdaptable)` when there are more.** Extra tabs hide behind More. 17. **Tabs navigate; they never perform actions.** "New Post" is a toolbar button. 18. **Use `NavigationSplitView` for iPad and Mac browse-and-detail.** It collapses to a stack on iPhone for free. 19. **Lay out by available space, not device.** Size classes and `ViewThatFits`; iPad apps run in narrow windows. 20. **Keep the system back button.** A custom "Back" label makes people stop and check. 21. **Two or three toolbar actions; the rest go in a `Menu`.** Plain SF Symbols, no `.circle` variants under glass. ## Text, color, and symbols 22. **Use text styles (`.body`, `.headline`), never `.system(size:)`.** Fixed sizes ignore Dynamic Type; use `@ScaledMetric` for custom spacing and sizes. 23. **Let text wrap, and stack rows at accessibility sizes.** Check `dynamicTypeSize.isAccessibilitySize`. 24. **Use semantic colors (`.primary`, `.secondary`, system backgrounds).** They adapt to Dark Mode and Increase Contrast; `.black` and `.white` don't. 25. **Never use color as the only signal.** Add a symbol or a word. 26. **Keep text contrast at 4.5:1 or better, checked in both appearances.** 27. **Use SF Symbols with their usual meaning.** `trash` deletes, `square.and.arrow.up` shares. 28. **Liquid Glass on controls, materials on content.** Glass on cards or rows buries the content. ## Motion and feedback 29. **Respect Reduce Motion:** swap slides and zooms for a fade (`@Environment(\.accessibilityReduceMotion)`). 30. **Never make people wait for an animation.** Keep feedback short and interruptible. 31. **Pair every haptic with something visible.** iPad and Mac have none. ## Mac 32. **Settings live in a `Settings` scene** (opened with ⌘,), not a sheet or a tab in the main window. 33. **Every command goes in the menu bar** via `.commands`; disable items that don't apply instead of hiding them. 34. **Keep standard shortcuts standard;** build new ones on ⌘, add ⇧ for variants. 35. **End a command title with "…" when it asks for more input** before acting (Export…). 36. **Title windows with their content, not the app name**, and let the system draw the window chrome. ## Apple HIG - https://developer.apple.com/design/human-interface-guidelines/buttons.md - https://developer.apple.com/design/human-interface-guidelines/modality.md - https://developer.apple.com/design/human-interface-guidelines/alerts.md - https://developer.apple.com/design/human-interface-guidelines/tab-bars.md - https://developer.apple.com/design/human-interface-guidelines/accessibility.md - https://developer.apple.com/design/human-interface-guidelines/typography.md - https://developer.apple.com/design/human-interface-guidelines/the-menu-bar.md - https://developer.apple.com/design/human-interface-guidelines/settings.md --- id: liquid-glass title: Liquid Glass platforms: [iOS, iPadOS, macOS] availability: iOS 26, macOS 26 reviewed: 2026-10-26 --- # Liquid Glass Liquid Glass is the iOS/macOS 26 material for the **controls layer**: bars, toolbars, tab bars, sheets, menus, and buttons that float above content. Most of it is automatic. Your job is mostly to stop blocking it, and to use it sparingly on your own controls. ## Rules 1. **Build with the current SDK, then delete custom bar chrome.** System bars, toolbars, tab bars, sheets, and menus adopt glass when built with Xcode 26+. Remove `.toolbarBackground(...)`, `UINavigationBarAppearance` / `UITabBarAppearance` setup, and solid `.presentationBackground` on sheets; they hide the glass. 2. **Glass only on the controls layer.** Never on list rows, cards, or content. Content that needs a backdrop gets a material (`.regularMaterial`). 3. **System button styles first.** `.buttonStyle(.glass)` for most, `.glassProminent` for the one main action per screen. 4. **Custom controls:** `.glassEffect(_:in:)`, applied after `.padding()` and `.frame()` (like a background). Default shape is a capsule. - `.regular` by default; `.clear` only over busy photos or video; `.identity` turns it off without changing layout. - `.tint(_:)` only to carry meaning or mark the primary action. `.interactive()` makes it react to touch (iOS). 5. **Group nearby glass in one `GlassEffectContainer(spacing:)`.** Glass can't sample other glass, so separate effects look wrong side by side. A container also lets shapes blend and morph. 6. **Morph with ids.** Inside a container, give views `.glassEffectID(_:in:)` with a `@Namespace` and change them inside `withAnimation`. `.glassEffectUnion(id:namespace:)` merges distant views into one shape. 7. **Toolbars:** use plain SF Symbols (no `.circle` variants; the glass is the container). Split groups with `ToolbarSpacer(.fixed)` or `ToolbarSpacer(.flexible)`. Take one item out of the shared background with `.sharedBackgroundVisibility(.hidden)`. 8. **Edges:** custom bars go in `.safeAreaBar(edge:)` so the scroll edge effect sees them. `.scrollEdgeEffectStyle(.hard, for: .top)` for dense UI like tables. `.backgroundExtensionEffect()` lets hero art extend under a sidebar or inspector. 9. **Tab bars (iOS):** `.tabBarMinimizeBehavior(.onScrollDown)`, `.tabViewBottomAccessory { }` for a mini player, `Tab(role: .search)` for the search tab. 10. **Concentric corners:** use `ConcentricRectangle()` (or `.rect(corners: .concentric)`) so inner shapes follow the container's radius. 11. **Accessibility:** system glass handles Reduce Transparency and Increase Contrast. Check custom glass with both on, and keep text on glass at a text style, not a fixed size. 12. **The compatibility opt-out is gone.** `UIDesignRequiresCompatibility` is ignored when you build with the 27 SDKs, so fix layouts rather than opting out. ## Default code ```swift ios struct PhotoControls: View { @Namespace private var glass @State private var isEditing = false var body: some View { GlassEffectContainer(spacing: 16) { HStack(spacing: 16) { if isEditing { Button("Crop", systemImage: "crop") { } .labelStyle(.iconOnly) .padding() .glassEffect(.regular.interactive()) .glassEffectID("crop", in: glass) } Button(isEditing ? "Done" : "Edit", systemImage: "pencil") { withAnimation { isEditing.toggle() } } .labelStyle(.iconOnly) .padding() .glassEffect(.regular.interactive()) .glassEffectID("edit", in: glass) } } } } ``` ## Supporting OS versions before 26 Gate glass and fall back to a material. Keep the shape the same so layout doesn't jump. ```swift extension View { @ViewBuilder func floatingBackground() -> some View { if #available(iOS 26, macOS 26, *) { glassEffect(.regular, in: .capsule) } else { background(.regularMaterial, in: .capsule) } } } ``` ## Pitfalls - Glass on glass (a glass button inside a glass card) looks muddy. One layer. - Many separate `.glassEffect` views without a container cost more to render and can't blend. - `.clear` glass over plain backgrounds is nearly invisible; add a dimming layer when the media behind is bright. ## Apple docs - https://developer.apple.com/documentation/technologyoverviews/adopting-liquid-glass.md - https://developer.apple.com/documentation/swiftui/applying-liquid-glass-to-custom-views.md - https://developer.apple.com/documentation/swiftui/view/glasseffect(_:in:).md - https://developer.apple.com/documentation/swiftui/glasseffectcontainer.md - https://developer.apple.com/design/human-interface-guidelines/materials.md --- id: review-checklist title: SwiftUI review checklist platforms: [iOS, iPadOS, macOS] reviewed: 2026-10-26 --- # SwiftUI review checklist Run this on any SwiftUI change before calling it done. Copy the list into your notes and tick items as you go. Fix what you find; don't just report it. ## 1. Run the linter It ships with this skill and needs only `python3`. ```sh scripts/check-swift.sh --target "iOS 17" path/to/Sources ``` Pass your real deployment target so it skips fixes your OS floor can't use. Fix every `error` and `warning`; judge each `info`. Silence a deliberate exception with `// check-swift:ignore ` on that line, and say why in the comment. ## 2. Checklist ```text Deprecated and outdated APIs [ ] No NavigationView, tabItem, ActionSheet, Alert(title:), presentationMode [ ] No foregroundColor, cornerRadius, accentColor(_:), edgesIgnoringSafeArea [ ] onChange uses the two- or zero-parameter closure [ ] .animation(_:value:) or withAnimation, never .animation(_:) alone [ ] No Text + Text; interpolate instead [ ] No UIScreen.main; no device checks for layout [ ] New code uses @Observable + @State/@Bindable/@Environment, not ObservableObject [ ] Async work in .task, not DispatchQueue or Task in onAppear [ ] #Preview, not PreviewProvider Availability [ ] Every API newer than the deployment target sits behind if #available / @available [ ] Each #available branch has a real fallback (e.g. glass falls back to a material) [ ] Builds with zero new warnings Accessibility [ ] Every icon-only control has a text label (Button("Title", systemImage:) or .accessibilityLabel) [ ] Tappable things are Buttons, not onTapGesture [ ] Targets are at least 44×44 pt (28×28 on Mac) [ ] Decorative images are hidden (Image(decorative:) or .accessibilityHidden(true)) [ ] Section titles have .accessibilityAddTraits(.isHeader) [ ] Color is never the only signal [ ] Motion respects accessibilityReduceMotion Dynamic Type and layout [ ] Text uses text styles, not .system(size:); custom sizes use @ScaledMetric [ ] Nothing truncates at the largest accessibility size; rows stack when needed [ ] No fixed frames on text; layouts adapt to size class, not device [ ] Content stays inside the safe area; backgrounds may extend past it Previews and state [ ] Each new view has a #Preview, with @Previewable @State for bindings [ ] Previews cover empty, loading, error, and long-text states where they apply [ ] @State is private; models are injected, not created in init [ ] List and ForEach use stable Identifiable ids, not indices HIG decisions [ ] One prominent action per screen; destructive actions use role: .destructive [ ] Push vs sheet vs full-screen cover matches the task [ ] Editing sheets have Cancel and a confirming action [ ] Alerts only for things people must act on; titles say what happened [ ] Tabs navigate and never act; five or fewer on iPhone [ ] Glass only on floating controls; semantic colors and SF Symbols elsewhere [ ] Mac: commands in the menu bar, Settings scene, standard shortcuts ``` ## 3. Verify in the running app - Build and run on the lowest supported OS and the newest. - Try the largest Dynamic Type size, Dark Mode, Increase Contrast, Reduce Transparency, and Reduce Motion. - Turn on VoiceOver and complete the main task once. - On iPad, try a narrow window; on Mac, resize the window and use only the keyboard. ## More - `deprecated-apis` for every correction and its minimum OS. - `hig-decisions` for the design rules and their reasons. - `liquid-glass`, `whats-new-26`, `whats-new-27` for newer APIs. - https://developer.apple.com/design/human-interface-guidelines/accessibility.md --- id: whats-new-26 title: What's new in SwiftUI 26 platforms: [iOS, iPadOS, macOS] availability: iOS 26, macOS 26 (Xcode 26) reviewed: 2026-10-26 --- # What's new in SwiftUI 26 APIs from the 2025 releases that models often don't know. Everything here needs iOS 26 / macOS 26 unless noted; gate with `if #available(iOS 26, macOS 26, *)` when you deploy earlier. Liquid Glass has its own topic: `liquid-glass`. ## Use these | Need | API | |---|---| | Glass on custom controls | `.glassEffect(_:in:)`, `GlassEffectContainer`, `.buttonStyle(.glass / .glassProminent)` (see `liquid-glass`) | | Space between toolbar groups | `ToolbarSpacer(.fixed)` / `ToolbarSpacer(.flexible)` | | Search in the bottom toolbar (iPhone) | `DefaultToolbarItem(kind: .search, placement: .bottomBar)` | | Collapse search to a button | `.searchToolbarBehavior(.minimize)` | | A subtitle under the title | `.navigationSubtitle(_:)` (macOS had it since 11; iOS 26) | | Zoom a sheet out of a toolbar button | `.matchedTransitionSource(id:in:)` on the `ToolbarItem` + `.navigationTransition(.zoom(sourceID:in:))` (iOS) | | Shrink the tab bar on scroll | `.tabBarMinimizeBehavior(.onScrollDown)` (iOS) | | A mini player above the tab bar | `.tabViewBottomAccessory { }`; read `\.tabViewBottomAccessoryPlacement` to adapt (iOS) | | A custom bar with the scroll edge effect | `.safeAreaBar(edge:) { }` | | Crisp or soft scroll edges | `.scrollEdgeEffectStyle(.hard / .soft, for:)` | | Art that extends under a sidebar | `.backgroundExtensionEffect()` | | Corners that follow their container | `ConcentricRectangle()`, `.rect(corners: .concentric)` | | Rich text editing | `TextEditor(text: $attributedString, selection: $selection)` with `AttributedString` and `AttributedTextSelection` | | A find bar in Mac text views | `.findNavigator(isPresented:)` (iOS since 16, macOS 26), `FindContext` | | Web content | `WebView(url:)` or `WebView(page)` with an `@Observable` `WebPage` (`import WebKit`) | | Custom animatable data | `@Animatable` macro on the view or shape; `@AnimatableIgnored` for skipped properties | | Slider tick marks | automatic when you pass `step:` | | Multi-item drag | `.draggable(containerItemID:containerNamespace:)` + `.dragContainer(for:itemID:in:_:)` | | Window resize anchor (macOS, iPadOS) | `.windowResizeAnchor(_:)` | | Assistive Access scene (iOS) | `AssistiveAccess { }` scene | | Equal-width buttons | `.buttonSizing(.flexible)` | ## Behavior changes to expect - System bars, sheets, menus, and controls render in Liquid Glass when built with Xcode 26+. Remove custom bar backgrounds that hide it. - iPadOS 26 shows a menu bar: commands you add with `.commands { }` appear there, so iPad apps should define them like Mac apps. - Deprecated in 26: `Text + Text` concatenation (interpolate instead) and `UIScreen.main`. See `deprecated-apis`. ## Examples ```swift ios struct Library: View { @State private var query = "" var body: some View { TabView { Tab("Books", systemImage: "books.vertical") { NavigationStack { List(0..<50) { Text("Book \($0)") } .navigationTitle("Books") .navigationSubtitle("50 titles") } } Tab(role: .search) { NavigationStack { Text("Results for \(query)") .searchable(text: $query) } } } .tabBarMinimizeBehavior(.onScrollDown) .tabViewBottomAccessory { Label("Now Reading", systemImage: "book") } } } ``` ```swift @Animatable struct Wave: Shape { var amplitude: Double @AnimatableIgnored var label = "wave" func path(in rect: CGRect) -> Path { var path = Path() path.move(to: CGPoint(x: rect.minX, y: rect.midY)) path.addQuadCurve( to: CGPoint(x: rect.maxX, y: rect.midY), control: CGPoint(x: rect.midX, y: rect.midY - amplitude) ) return path } } ``` ## Apple docs - https://developer.apple.com/documentation/updates/swiftui.md (the June 2025 section) - https://developer.apple.com/documentation/swiftui/toolbarspacer.md - https://developer.apple.com/documentation/swiftui/view/tabviewbottomaccessory(content:).md - https://developer.apple.com/documentation/webkit/webview-swift.struct.md --- id: whats-new-27 title: What's new in SwiftUI 27 platforms: [iOS, iPadOS, macOS] availability: iOS 27, macOS 27 (Xcode 27); some items 27.1 reviewed: 2026-10-26 --- # What's new in SwiftUI 27 The 2026 releases, checked against the iOS 27.1 / macOS 27 SDKs. Gate with `if #available(iOS 27, macOS 27, *)` (or `@available` on a type) when you deploy earlier. Items marked 27.1 need `#available(iOS 27.1, macOS 27.1, *)`. ## Changes you get by building with Xcode 27 - **`@State` is a macro.** A class stored in `@State` is created once, not on every parent update. You can drop workarounds that lazily created `@Observable` models in `.task` or `onAppear`. - **`@ContentBuilder`** is the one builder for views, toolbars, and commands (today a typealias of `ViewBuilder`). Existing `@ViewBuilder` code keeps working. - **Item and error alerts:** `.alert(_:item:actions:)`, `.alert(error:actions:)`, and `.confirmationDialog(_:item:titleVisibility:actions:)` drive the dialog from an optional. The `item:` forms back-deploy; `alert(error:)` with a plain `Error` needs 27. - **Deprecated:** `FileDocument` / `ReferenceFileDocument` (use `Document`) and `RoundedBorderTextFieldStyle` (use `.textFieldStyle(.bordered)` plus `.textInputBorderShape(.roundedRectangle)`). See `deprecated-apis`. ## New APIs | Need | API | |---|---| | Drag to reorder in any stack, grid, or layout | `ForEach(...).reorderable()` + `.reorderContainer(for:move:)` | | Swipe actions outside `List` | `.swipeActions(edge:allowsFullSwipe:content:onPresentationChanged:)` + `.swipeActionsContainer()` on the scroll view | | Keep key toolbar items when space runs out | `.visibilityPriority(.high)` on toolbar content | | Secondary actions in the overflow menu (iOS) | `ToolbarOverflowMenu { }` | | Pin an item to the trailing edge (iOS) | `ToolbarItem(placement: .topBarPinnedTrailing)` | | Minimize toolbars on scroll | `.toolbarMinimizationBehavior(.onScrollDown, for: .navigationBar)` | | A tab set apart at the trailing end | `Tab("New", systemImage: "plus", role: .prominent) { }` | | Cache `AsyncImage` downloads | `.asyncImageURLSession(session)` or `AsyncImage(request:)` | | A sheet that fades in (iOS) | `.navigationTransition(.crossFade)` on the sheet content | | URL-based documents | `ReadableDocument`, `WritableDocument`, `Document`; `FileWrapperDocumentReader/Writer`; `URLDocumentConfiguration` | | Limit gesture input sources | `GestureInputKinds` on tap, drag, long-press, magnify, and rotate gestures | | Adaptive primary/secondary layout (27.1) | `ArrangementView` + `.arrangementViewStyle(.split)` or `.overlay` | | Avoid camera housings and hinges (27.1) | `ReservedRegion`, `GeometryProxy.reservedRegions(kind:options:layoutDirectionBehavior:)` | | Toolbars on the vertical axis (27.1) | `\.toolbarVerticalEdge`, `.toolbarVerticalBehavior(_:)`, `.axisBehavior(_:)` | ## Examples ```swift @available(iOS 27, macOS 27, *) struct Board: View { struct Card: Identifiable { let id = UUID() var title: String } @State private var cards = [Card(title: "Plan"), Card(title: "Build"), Card(title: "Ship")] var body: some View { VStack { ForEach(cards) { Text($0.title) } .reorderable() } .reorderContainer(for: Card.self) { difference in let moving = cards.filter { difference.sources.contains($0.id) } cards.removeAll { difference.sources.contains($0.id) } if case .before(let id) = difference.destination.position, let index = cards.firstIndex(where: { $0.id == id }) { cards.insert(contentsOf: moving, at: index) } else { cards.append(contentsOf: moving) } } } } ``` ```swift ios @available(iOS 27, *) struct MessageToolbar: View { var body: some View { NavigationStack { List(0..<40) { Text("Message \($0)") } .toolbar { ToolbarItem(placement: .topBarPinnedTrailing) { Button("Compose", systemImage: "square.and.pencil") { } } ToolbarItem { Button("Filter", systemImage: "line.3.horizontal.decrease") { } } .visibilityPriority(.high) ToolbarOverflowMenu { Button("Archive All", systemImage: "archivebox") { } Button("Mark All as Read", systemImage: "envelope.open") { } } } .toolbarMinimizationBehavior(.onScrollDown, for: .navigationBar) } } } ``` ## Apple docs - https://developer.apple.com/documentation/updates/swiftui.md (June and September 2026) - https://developer.apple.com/documentation/swiftui/view/reordercontainer(for:isenabled:move:).md - https://developer.apple.com/documentation/swiftui/toolbaroverflowmenu.md - https://developer.apple.com/documentation/swiftui/document.md - https://developer.apple.com/documentation/swiftui/arrangementview.md --- id: button title: Button platforms: [iOS, iPadOS, macOS] availability: iOS 13.0+, macOS 10.15+, visionOS 1.0+ docs: https://developer.apple.com/documentation/swiftui/button.md hig: https://developer.apple.com/design/human-interface-guidelines/buttons.md --- # Button A control that runs an action when tapped. ## Contents - Don't use when - Default code - Rules - Pitfalls - Key APIs - Variants - On macOS - Related ## Don't use when - Moving to another screen: use `NavigationLink(value:)` with `.navigationDestination(for:)`. - Opening a web page or another app: use `Link`. - Switching a setting on or off: use `Toggle`. ## Default code ```swift Button("Sign In") { signIn() } .buttonStyle(.borderedProminent) ``` **Note:** The trailing closure is the action. For a custom look, use `Button { … } label: { … }` and pass any view as the label. ## Rules - **Use one prominent button per screen.** The filled `.borderedProminent` style marks the action people most likely want. Several of them compete, and none stands out. - **Use style, not size, to show the preferred choice.** Buttons of the same size read as one set of options. Make the preferred one more prominent instead of bigger. - **Start labels with a verb.** A label like “Add to Cart” says what will happen. A noun or “OK” makes people guess. - **Make every button at least 44 × 44 points.** Smaller targets are easy to miss. The system styles handle this; for a custom label, add padding or `.contentShape(.rect)` to grow the tappable area. - **Never make a destructive action the default.** People tap the prominent button without reading it. Keep Delete in a regular style with `role: .destructive`, and confirm before it runs. - **Pick glass styles for floating controls and bordered styles for content.** On iOS and macOS 26+, a button that floats above content uses `.buttonStyle(.glass)`, or `.glassProminent` for the one main action. Buttons inside content keep `.bordered` and `.borderedProminent`. Glass belongs to the controls layer (see `liquid-glass`). ## Pitfalls - **Set a style once for a whole group.** `.buttonStyle(_:)` applies to every button inside a container, so put it on the `VStack` or `HStack` instead of each button. - **In a `List` or `Form`, buttons become rows.** They draw as tappable rows with tinted text, not the bordered styles above. ## Key APIs `Button(_:action:)`, `.buttonStyle(_:)`, `.controlSize(_:)`, `.buttonBorderShape(_:)`, `role:`, `.disabled(_:)` ## Variants ### Full-width button Put `.frame(maxWidth: .infinity)` on the label, not the button, so the whole bar fills and responds to taps. ```swift Button { // Continue } label: { Text("Continue") .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .controlSize(.large) ``` ## On macOS A push button that runs an action when someone clicks it. ### Default code ```swift Button("Add to Library") { addToLibrary() } ``` **Note:** The action is a closure, like an `onClick` handler. With an icon, use `Button(_:systemImage:action:)` so toolbars and menus can show the right parts. ### Rules - **Mark the preferred choice with style, not size.** Buttons of the same size read as one set of choices. Give the likely one a more prominent style instead of making it bigger. - **Never make a destructive button the default.** People often press Return or click the highlighted button without reading it. Make the safe choice the default so a quick Return doesn't delete anything. - **End the title with an ellipsis when it opens another window.** On the Mac, “…” tells people they'll be asked for more before anything happens, as in Export… or Choose Folder…. - **Label buttons with a verb.** A short title that starts with a verb, like Add to Library, says what will happen. For a familiar action like sharing, a familiar SF Symbol works too. ### Pitfalls - **A style on a container reaches every button inside it.** Put `.buttonStyle(_:)` on an `HStack` or a `Form` instead of on each button. - **Give icon-only buttons a title anyway.** `Button(_:systemImage:action:)` with `.labelStyle(.iconOnly)` shows just the symbol, and VoiceOver still reads the title. ### Key APIs `Button(_:systemImage:action:)`, `Button(_:role:action:)`, `.keyboardShortcut(.defaultAction)`, `.keyboardShortcut(.cancelAction)`, `.help(_:)` ### Variants #### Default and cancel buttons Add `.keyboardShortcut(.defaultAction)` to the likeliest button so Return clicks it and tints it, and `.cancelAction` so Escape cancels. ```swift HStack { Button("Cancel", role: .cancel) { dismiss() } .keyboardShortcut(.cancelAction) Button("Save") { save() } .keyboardShortcut(.defaultAction) } ``` **Note:** If more than one button has `.defaultAction`, only the first gets the tint. More variants, left out to keep this file short: Icon button, Loading state, Destructive with confirmation, Destructive actions (macOS), Small buttons that act on a list (macOS), A button that looks like a link (macOS), Scope bar buttons (macOS). Apple's docs: https://developer.apple.com/documentation/swiftui/button.md ## Related `toggle`, `menu`, `link`, `control-group`, `keyboard-shortcuts`, `alert`, `sheet`, `tooltips` --- id: toggle title: Toggle platforms: [iOS, iPadOS, macOS] availability: iOS 13.0+, macOS 10.15+, visionOS 1.0+ docs: https://developer.apple.com/documentation/swiftui/toggle.md hig: https://developer.apple.com/design/human-interface-guidelines/toggles.md --- # Toggle A control that switches between on and off. ## Contents - Default code - Rules - Pitfalls - Key APIs - Variants - On macOS - Related ## Default code ```swift @State private var wifiOn = true Form { Toggle("Wi-Fi", isOn: $wifiOn) } ``` **Note:** Pass the binding with `$`. The toggle writes the new value back to your `@State`, like a controlled `` that updates its own state. ## Rules - **Use a switch only in a list or form row.** A switch floating on its own looks out of place. Outside a list, use `.toggleStyle(.button)` instead, like a filter button. - **Name the setting, not its state.** The switch already shows on or off. A label like “Wi-Fi” stays true either way; “Turn On Wi-Fi” is wrong half the time. - **Change the tint only when you need to.** The default green reads well everywhere. If you use your accent color, check it still stands out from the gray off state. - **Show the state with more than color.** For a button-style toggle, also switch the symbol, like `star` to `star.fill`, so people who can't see the color difference can still tell. ## Pitfalls - **Always give a toggle a real label.** `.labelsHidden()` hides it on screen but VoiceOver still reads it, so a toggle without one is a mystery switch. ## Key APIs `Toggle(_:isOn:)`, `Toggle(_:systemImage:isOn:)`, `.toggleStyle(_:)`, `.tint(_:)`, `.labelsHidden()`, `ToggleStyle` ## Variants ### Settings list Inside a `Form`, each toggle becomes a row. Use `.disabled(_:)` on a setting that depends on another. ```swift @State private var notifications = true @State private var sounds = false Form { Toggle("Notifications", systemImage: "bell.badge", isOn: $notifications) Toggle("Sounds", systemImage: "speaker.wave.2", isOn: $sounds) .disabled(!notifications) } ``` ### Formatting buttons Use `.toggleStyle(.button)` outside a list. The button stays highlighted while on, like Bold in a text editor. ```swift @State private var isBold = true @State private var isItalic = false HStack { Toggle("Bold", systemImage: "bold", isOn: $isBold) Toggle("Italic", systemImage: "italic", isOn: $isItalic) } .toggleStyle(.button) .labelStyle(.iconOnly) ``` ## On macOS A checkbox, switch, or button that turns a setting on or off. ### Default code ```swift @State private var showsInMenuBar = true Toggle("Show in menu bar", isOn: $showsInMenuBar) ``` **Note:** Pass the binding with `$`. The toggle writes the new value back to your `@State`, like a controlled ``. ### Rules - **Keep checkboxes and switches in the window body.** They belong next to the content they change, not in a toolbar. In a toolbar, use a button-style toggle instead. - **Use checkboxes to show a hierarchy of settings.** Checkboxes line up along their leading edge, so indenting the ones that depend on another shows how they're related. - **Save switches for settings that carry weight.** A switch looks heavier than a checkbox, so it suits a setting that turns on a whole feature or a group of other settings. Don't swap an existing checkbox for one. - **Show a mixed state when sub-options disagree.** A checkbox that controls several others should show a dash when some are on and some are off. `Toggle(sources:isOn:)` does it for you. ### Pitfalls - **The default style depends on where the toggle is.** It's a checkbox in most places, and a switch in a grouped `Form`. Set `.toggleStyle(_:)` when you need one or the other. - **Clicking the label toggles it too.** The title is part of the checkbox, so people don't have to aim for the small box. ### Key APIs `.toggleStyle(.checkbox)`, `.toggleStyle(.switch)`, `.toggleStyle(.button)`, `Toggle(sources:isOn:)`, `.controlSize(_:)` ### Variants #### Settings with dependencies Outside a grouped form, toggles are checkboxes. Indent the ones that depend on another, and disable them while it's off. ```swift @State private var showsWarnings = true @State private var includesMinor = false @State private var playsSound = true VStack(alignment: .leading) { Toggle("Show warnings", isOn: $showsWarnings) Group { Toggle("Include minor issues", isOn: $includesMinor) Toggle("Play a sound", isOn: $playsSound) } .padding(.leading, 20) .disabled(!showsWarnings) } ``` More variants, left out to keep this file short: Checkbox-style agreement, Select all, with a mixed state (macOS), Switches in a grouped form (macOS), A toggle in the toolbar (macOS). Apple's docs: https://developer.apple.com/documentation/swiftui/toggle.md ## Related `button`, `form`, `state`, `binding` --- id: picker title: Picker platforms: [iOS, iPadOS, macOS] availability: iOS 13.0+, macOS 10.15+, visionOS 1.0+ docs: https://developer.apple.com/documentation/swiftui/picker.md hig: https://developer.apple.com/design/human-interface-guidelines/pickers.md --- # Picker A control for choosing one value from a list of options. ## Contents - Default code - Rules - Pitfalls - Key APIs - On macOS - Related ## Default code ```swift @State private var size = "Medium" Picker("Size", selection: $size) { Text("Small").tag("Small") Text("Medium").tag("Medium") Text("Large").tag("Large") } ``` **Important:** Each option's `.tag` must be the same type as `selection`. With an optional selection, tag with `Optional("Small")` or nothing shows as selected. ## Rules - **Use a segmented picker for two to five short options.** More segments get cramped on iPhone and the labels get cut off. For longer lists, use the `.menu` style. - **Use all text or all icons in one segmented control.** Mixing them makes segments look uneven and hard to compare. - **Order options so people can guess what's hidden.** Alphabetical, numeric, or by time. A menu or wheel hides most options, so a predictable order makes them quick to find. - **Use a segmented control to switch views, not whole sections.** Switching between related views on one screen fits. For the app's main sections, use a TabView (`tab-view`). ## Pitfalls - **The tag's type must match the selection exactly.** A `String` tag on an `Int` selection compiles, but nothing ever looks selected. - **The default look depends on where the picker lives.** On its own it's a menu button; in a `Form` it's a row with the value on the right. ## Key APIs `Picker(_:selection:content:)`, `.tag(_:)`, `.pickerStyle(_:)`, `ForEach(Enum.allCases)`, `.labelsHidden()`, `Menu { Picker }` ## On macOS A pop-up button, radio group, or segmented control for one choice from a set. ### Default code ```swift enum SortKey: String, CaseIterable, Identifiable { case name, date, size, kind var id: Self { self } } @State private var sortKey = SortKey.name Picker("Sort by", selection: $sortKey) { ForEach(SortKey.allCases) { order in Text(order.rawValue.capitalized) } } ``` **Note:** Like a `` with a bound value: the slider reads `volume` and writes it back as people drag. ### Rules - **Put the minimum on the leading side.** People expect a horizontal slider to grow from leading to trailing, like 0% to 100%. Reversing it makes every adjustment feel backward. - **Show the result while people drag.** Update what the slider controls as the value changes, the way the Dock grows while you drag its Size slider. Save heavy work for when the drag ends. - **Use tick marks when exact values matter, and label only what helps.** Ticks show the scale and make specific values easy to hit. Labeling the minimum and maximum is often enough. - **Offer a text field when people need a precise value.** For a wide range, pair the slider with a field, and maybe a Stepper (`stepper`), so people can type the exact number. ### Pitfalls - **A `step` adds tick marks.** On the Mac, a slider with a step draws a tick at each one, and the value moves in whole steps. - **The value is a floating-point number.** Bind a `Double` (or another `BinaryFloatingPoint`); round it yourself if you store an integer. - **`step` also sets how far VoiceOver moves it.** When people adjust the slider with VoiceOver, it changes by one step. ### Key APIs `Slider(value:in:step:)`, `minimumValueLabel:`, `maximumValueLabel:`, `ticks: { SliderTick(…) }`, `.controlSize(_:)` ### Variants #### Symbols at each end `minimumValueLabel` and `maximumValueLabel` show what the ends mean, like a small and a large photo for icon size. ```swift @State private var iconSize = 48.0 Slider(value: $iconSize, in: 16...128) { Text("Icon size") } minimumValueLabel: { Image(systemName: "photo") .imageScale(.small) } maximumValueLabel: { Image(systemName: "photo") .imageScale(.large) } ``` #### Tick marks with labels In macOS 26, pass `ticks:` to place a `SliderTick` at each value that matters, labeling only the ones that help, like the ends. ```swift @State private var quality = 0.75 Slider(value: $quality) { Text("Quality") } ticks: { SliderTick(0.0) { Text("Small file") } SliderTick(0.25) SliderTick(0.5) SliderTick(0.75) SliderTick(1.0) { Text("Best") } } ``` More variants, left out to keep this file short: Brightness with icons, Act when the drag ends, Tick marks, Fill from the middle, Live preview, save at the end (macOS), With a field and a stepper (macOS). Apple's docs: https://developer.apple.com/documentation/swiftui/slider.md ## Related `stepper`, `progress-view`, `gauge`, `state`, `gauge-progress`, `form`, `labeled-content`, `text-field` --- id: stepper title: Stepper platforms: [iOS, iPadOS, macOS] availability: iOS 13.0+, macOS 10.15+, visionOS 1.0+ docs: https://developer.apple.com/documentation/swiftui/stepper.md hig: https://developer.apple.com/design/human-interface-guidelines/steppers.md --- # Stepper A pair of minus and plus buttons that change a number by a fixed amount. ## Contents - Default code - Rules - Pitfalls - Key APIs - Variants - On macOS - Related ## Default code ```swift @State private var guests = 2 Stepper("Guests: \(guests)", value: $guests, in: 1...10) ``` **Important:** The stepper only draws the buttons. Put the value in the label yourself, or people can't see what they're changing. ## Rules - **Show the value next to the stepper.** The stepper only draws minus and plus. Without the number in view, people can't tell what they're changing. - **Use a stepper for small changes.** A few taps is fine; fifty is not. When values vary widely, pair it with a text field, as in the example above. ## Pitfalls - **Always pass a range with `in:`.** The minus or plus button dims at each end, so people can't order zero or a negative amount. ## Key APIs `Stepper(_:value:in:)`, `step:`, `Stepper(value:in:step:label:)`, `onIncrement:` / `onDecrement:`, `.labelsHidden()` ## Variants ### Quantity in a cart Use `.labelsHidden()` and show the value in your own layout, like a quantity in a cart. ```swift @State private var quantity = 1 HStack { VStack(alignment: .leading) { Text("Cold Brew").font(.headline) Text("$\(quantity * 4).00").foregroundStyle(.secondary) } Spacer() Text("\(quantity)") .monospacedDigit() Stepper("Quantity", value: $quantity, in: 1...9) .labelsHidden() } ``` **Note:** Keep a real label like “Quantity” even when hidden. VoiceOver reads it so people know what the buttons change. ### Decimal steps Bind a `Double` and pass `step: 0.5` to change the value by half an hour per tap. ```swift @State private var hours = 8.0 Stepper(value: $hours, in: 4...12, step: 0.5) { Text("Sleep goal: \(hours.formatted()) hours") } ``` ## On macOS Up and down arrows that change a number by a fixed amount. ### Default code ```swift @State private var copies = 1 LabeledContent("Copies:") { HStack(spacing: 4) { TextField("Copies", value: $copies, format: .number) .frame(width: 44) Stepper("Copies", value: $copies, in: 1...99) } .labelsHidden() } ``` **Note:** The field and the stepper share one binding, so typing a number and clicking the arrows stay in sync, like an ``. ### Rules - **Make the value it changes obvious.** A stepper doesn't show a number of its own. Keep the value right beside it so people see what each click does. - **Pair it with a field when big changes are likely.** A few clicks are fine for small changes. When values vary a lot, like the number of copies to print, let people type the number too. - **Consider Shift-click for large ranges.** On the Mac, a Shift-click that moves by a bigger amount, like ten steps, saves people a lot of clicking. ### Pitfalls - **On the Mac, a stepper is just two small arrows.** It doesn't show the value, so always put the number next to it: in its label or in a field. - **A range keeps the value in bounds.** With `in: 1...99`, the arrows stop at the ends instead of going past them. ### Key APIs `Stepper(_:value:in:step:)`, `Stepper(_:onIncrement:onDecrement:)`, `TextField(_:value:format:)` ### Variants #### The value in the label When there's no room for a field, put the current value in the stepper's own label. ```swift @State private var margin = 1.0 Stepper(value: $margin, in: 0...3, step: 0.25) { Text("Margin: \(margin, format: .number.precision(.fractionLength(2))) in") } ``` #### Two values side by side Give each field its own stepper, and wrap each pair in `LabeledContent` so the rows line up with other form controls. ```swift @State private var width = 800 @State private var height = 600 Form { LabeledContent("Width:") { HStack(spacing: 4) { TextField("Width", value: $width, format: .number) .frame(width: 60) Stepper("Width", value: $width, in: 100...4000, step: 10) } .labelsHidden() } LabeledContent("Height:") { HStack(spacing: 4) { TextField("Height", value: $height, format: .number) .frame(width: 60) Stepper("Height", value: $height, in: 100...4000, step: 10) } .labelsHidden() } } ``` More variants, left out to keep this file short: With a text field, Step through a list, Your own steps (macOS). Apple's docs: https://developer.apple.com/documentation/swiftui/stepper.md ## Related `slider`, `text-field`, `form`, `state`, `labeled-content`, `date-picker` --- id: date-picker title: DatePicker platforms: [iOS, iPadOS, macOS] availability: iOS 13.0+, macOS 10.15+, visionOS 1.0+ docs: https://developer.apple.com/documentation/swiftui/datepicker.md hig: https://developer.apple.com/design/human-interface-guidelines/pickers.md --- # DatePicker A control for choosing a date, a time, or both. ## Contents - Default code - Rules - Pitfalls - Key APIs - Variants - On macOS - Related ## Default code ```swift @State private var dueDate = Date.now DatePicker("Due", selection: $dueDate, displayedComponents: .date) ``` **Note:** The value is always a full `Date`, a moment in time, even when you only show the day. Display it with `.formatted(date:time:)`, passing `.omitted` for the part you hide. ## Rules - **Show only the parts people need.** A birthday has no time, and an alarm has no date. Extra fields make the picker wider and invite wrong input. - **Use the compact style in forms.** It takes one row and opens a calendar when tapped. Save `.graphical` for screens where picking a date is the main task. - **Only allow dates that make sense.** Pass a range with `in:` so people can't pick a check-out before check-in, instead of showing an error after. ## Pitfalls - **The picker follows the device's language, calendar, and time zone.** Display dates with `.formatted(date:time:)` instead of building strings, and they'll match. ## Key APIs `DatePicker(_:selection:displayedComponents:)`, `in:`, `.datePickerStyle(_:)`, `.labelsHidden()`, `MultiDatePicker`, `.formatted(date:time:)` ## Variants ### Booking a stay Pass `in: checkIn...` to the check-out picker so a stay can't end before it begins. ```swift @State private var checkIn = Date.now @State private var checkOut = Date.now.addingTimeInterval(3 * 86_400) Form { DatePicker("Check-in", selection: $checkIn, in: Date.now..., displayedComponents: .date) DatePicker("Check-out", selection: $checkOut, in: checkIn..., displayedComponents: .date) let nights = Calendar.current.dateComponents([.day], from: checkIn, to: checkOut).day ?? 0 Text("\(nights) nights") .foregroundStyle(.secondary) } ``` **Important:** Don't count on the range to fix a date that's already set. If check-in moves past check-out, update check-out in `.onChange(of: checkIn)`. ### Time only, as a wheel Use `.hourAndMinute` with the `.wheel` style for a spinning clock, like the one in Alarms. ```swift @State private var alarm = Date.now DatePicker("Alarm", selection: $alarm, displayedComponents: .hourAndMinute) .datePickerStyle(.wheel) .labelsHidden() ``` ## On macOS A date and time field, or a calendar, for picking a moment in time. ### Default code ```swift @State private var dueDate = Date.now DatePicker("Due date", selection: $dueDate, displayedComponents: .date) ``` **Note:** The binding is a full `Date`. Showing only `.date` hides the time but doesn't remove it, so compare days with `Calendar` rather than `==`. ### Rules - **Choose the style for the job.** A text field fits a tight space and people who know the exact date and time. A calendar suits browsing through days before choosing one. - **Keep the picker next to what it changes.** Show it in place, in the form row or a popover, instead of switching to another view to pick a date. ### Pitfalls - **Without `displayedComponents`, you get both date and time.** Pass `.date` or `.hourAndMinute` to show only what people should edit. - **The order follows the person's region.** The same picker shows day, month, and year in the order their locale expects. - **The Mac default is a field with arrows.** Click a part, like the month or the minute, then type or click the arrows to change it. ### Key APIs `DatePicker(_:selection:)`, `displayedComponents:`, `in: Date.now...`, `.datePickerStyle(.stepperField)`, `.datePickerStyle(.field)`, `.datePickerStyle(.graphical)` ### Variants #### Only future dates Pass a range with `in:` to stop people from picking a date that makes no sense, like a deadline in the past. ```swift @State private var deadline = Date.now DatePicker("Deadline:", selection: $deadline, in: Date.now..., displayedComponents: .date) ``` #### A calendar to browse `.graphical` shows a month at a time, which suits a sidebar, inspector, or popover where people browse before choosing. ```swift @State private var day = Date.now DatePicker("Day", selection: $day, displayedComponents: .date) .datePickerStyle(.graphical) .labelsHidden() ``` #### Just a time Pass `.hourAndMinute` to show only a time, for something like a daily reminder where the date doesn't matter. ```swift @State private var reminder = Date.now DatePicker("Remind me at:", selection: $reminder, displayedComponents: .hourAndMinute) ``` More variants, left out to keep this file short: Birthday (past dates only), Several days with MultiDatePicker, A start and an end (macOS). Apple's docs: https://developer.apple.com/documentation/swiftui/datepicker.md ## Related `picker`, `form`, `state`, `stepper`, `popover`, `labeled-content` --- id: color-picker title: ColorPicker platforms: [iOS, iPadOS, macOS] availability: iOS 14.0+, macOS 11.0+, visionOS 1.0+ docs: https://developer.apple.com/documentation/swiftui/colorpicker.md hig: https://developer.apple.com/design/human-interface-guidelines/color-wells.md --- # ColorPicker A swatch that opens the system color picker so people can choose any color. ## Contents - Default code - Rules - Pitfalls - Key APIs - Variants - On macOS - Related ## Default code ```swift @State private var accent = Color.blue ColorPicker("Accent Color", selection: $accent) ``` ## Rules - **Use the system color picker.** People already know it, and it keeps their saved colors across apps. A custom picker throws that away. - **Offer a few swatches when the choices are fixed.** For five tag colors, a row of circles is faster than a full spectrum. Use a picker with `.palette` style or your own buttons. ## Pitfalls - **`Color` isn't `Codable`.** To save a choice, store `color.resolve(in: environment)`, a `Color.Resolved` that is (iOS 17+). - **You can't customize the sheet.** The grid, spectrum, sliders, and eyedropper are the system's; for your own UI, build swatches with buttons. ## Key APIs `ColorPicker(_:selection:)`, `supportsOpacity:`, `.labelsHidden()`, `Color.resolve(in:)`, `Color(_: Color.Resolved)` ## Variants ### Theme a card Bind each `ColorPicker` to a `Color` in `@State`, and use it right away for a live preview. ```swift @State private var background = Color.orange @State private var textColor = Color.white VStack { Text("Hello!") .font(.title.bold()) .foregroundStyle(textColor) .frame(maxWidth: .infinity, minHeight: 80) .background(background.gradient, in: .rect(cornerRadius: 16)) Form { ColorPicker("Background", selection: $background) ColorPicker("Text", selection: $textColor, supportsOpacity: false) } } ``` **Note:** The text picker passes `supportsOpacity: false`, since see-through text is rarely what people want. ### Just the swatch Use `.labelsHidden()` to show only the round swatch, next to the thing it colors. ```swift @State private var highlight = Color.yellow HStack { Text("Highlight") .padding(.horizontal, 6) .background(highlight.opacity(0.5), in: .rect(cornerRadius: 4)) Spacer() ColorPicker("Highlight color", selection: $highlight) .labelsHidden() } ``` **Note:** Give it a real label anyway, like “Highlight color”. VoiceOver reads it so people know what the swatch changes. ### Read the components Call `resolve(in:)` to get a color's red, green, and blue values, for example to show a hex code. ```swift @State private var highlight = Color.yellow @Environment(\.self) private var environment let resolved = highlight.resolve(in: environment) HStack { ColorPicker("Color", selection: $highlight) .labelsHidden() Text(String(format: "#%02X%02X%02X", Int(min(max(resolved.red, 0), 1) * 255), Int(min(max(resolved.green, 0), 1) * 255), Int(min(max(resolved.blue, 0), 1) * 255))) .monospaced() } ``` **Important:** Picked colors can be wide-gamut, with values below 0 or above 1. Clamp them before converting to hex. ## On macOS A color well that opens the system color panel to pick a color. ### Default code ```swift @State private var highlight = Color.yellow ColorPicker("Highlight", selection: $highlight) ``` **Note:** Click the well to open the system color panel. When people choose a color there, the well and your binding update to match. ### Pitfalls - **Color wells take drag and drop.** People can drag a color from one well to another, or from the color panel onto a well. ### Key APIs `supportsOpacity: false`, `ColorPicker(selection:label:)` ### Variants #### Colors in an inspector Put a few labeled wells in a `Form`, each its own `ColorPicker` with its own binding. ```swift @State private var fill = Color.teal @State private var stroke = Color.indigo Form { ColorPicker("Fill", selection: $fill) ColorPicker("Stroke", selection: $stroke) } ``` #### Solid colors only Pass `supportsOpacity: false` when a see-through color makes no sense, like a label color, and the panel leaves out its opacity controls. ```swift @State private var labelColor = Color.orange ColorPicker("Label color", selection: $labelColor, supportsOpacity: false) ``` **Note:** It also strips opacity from colors you set in code and from people's saved colors. #### Saving the color `Color` isn't `Codable`, so call `resolve(in:)` for a `Color.Resolved`, which you can save as JSON. ```swift @State private var tagColor = Color.mint @Environment(\.self) private var environment @AppStorage("tagColor") private var savedTag = "" ColorPicker("Tag color", selection: $tagColor) .onChange(of: tagColor) { let resolved = tagColor.resolve(in: environment) if let data = try? JSONEncoder().encode(resolved) { savedTag = String(decoding: data, as: UTF8.self) } } ``` ## Related `system-colors`, `picker`, `form`, `color`, `labeled-content`, `inspector` --- id: progress-view title: ProgressView platforms: [iOS, iPadOS] availability: iOS 14.0+, macOS 11.0+, visionOS 1.0+ docs: https://developer.apple.com/documentation/swiftui/progressview.md hig: https://developer.apple.com/design/human-interface-guidelines/progress-indicators.md --- # ProgressView Shows that work is happening, as a spinning indicator or a filling bar. Also called: spinner, loading, loader, activity indicator, progress bar, , UIActivityIndicatorView, UIProgressView, throbber. ## Contents - Default code - Rules - Pitfalls - Key APIs - Variants - Related ## Default code ```swift // Unknown length: a spinner ProgressView("Loading messages") // Known progress: a bar from 0 to 1 ProgressView(value: 0.4) ``` **Important:** On iOS, `.circular` is always a spinner, even with a value. For a ring that fills, use a Gauge (`gauge`) with the `.accessoryCircularCapacity` style. ## Rules - **Use a bar when you know how far along you are.** A spinner only says “wait”. A bar tells people how long, so they can decide whether to do something else. - **Say what's happening.** A specific label like “Importing 240 photos” is more useful than “Loading…”, which people learn to ignore. - **Let people cancel long tasks.** Add a Cancel button next to the progress, and confirm first if canceling throws away work. - **Don't switch from a spinner to a bar mid-task.** They're different shapes and sizes, so the swap jumps. Pick one, or start with an empty bar. ## Pitfalls - **Skip the spinner for very short waits.** If work finishes in under a second, a flash of spinner makes the app feel slower, not faster. ## Key APIs - `ProgressView()`: A spinner, for work of unknown length. - `ProgressView(value:total:)`: A bar that fills; `total` defaults to 1. - `label:` / `currentValueLabel:`: Text above and below the bar. - `ProgressView(timerInterval:)`: Fills or empties between two dates by itself. - `.controlSize(_:)`: Spinner size, from `.small` to `.extraLarge`. - `.tint(_:)`: The color of the bar or spinner. ## Variants ### Button that shows it's busy Swap a button's label for a `ProgressView` while work runs, and ignore taps until it's done. ```swift @State private var isLoading = false Button { guard !isLoading else { return } isLoading = true Task { try? await Task.sleep(for: .seconds(2)) isLoading = false } } label: { ZStack { Text("Save").opacity(isLoading ? 0 : 1) if isLoading { ProgressView() .tint(.white) } } .frame(width: 80) } .buttonStyle(.borderedProminent) .controlSize(.large) ``` **Note:** The text stays in the `ZStack` at zero opacity, so the button keeps its width when the spinner appears. ### Counts with a total Pass `total:` when progress is a count, like photos uploaded, instead of dividing yourself. ```swift @State private var uploaded = 3.0 VStack(spacing: 16) { ProgressView(value: uploaded, total: 8) { Text("Uploading photos") } currentValueLabel: { Text("\(Int(uploaded)) of 8") } .animation(.default, value: uploaded) Button(uploaded < 8 ? "Upload one more" : "Start over") { uploaded = uploaded < 8 ? uploaded + 1 : 0 } } ``` ### Countdown that runs by itself Use `ProgressView(timerInterval:)` to animate between two dates with no timer code. ```swift @State private var countdown = Date.now...Date.now.addingTimeInterval(30) VStack(spacing: 16) { ProgressView(timerInterval: countdown) { Text("Code expires") } Button("Restart") { countdown = Date.now...Date.now.addingTimeInterval(30) } } .onAppear { countdown = Date.now...Date.now.addingTimeInterval(30) } ``` **Note:** Pass `countsDown: false` to fill up instead. The same view works in widgets and Live Activities. ### Loading overlay Put a large spinner in an `.overlay` on a material, so the layout doesn't jump while content loads. ```swift RoundedRectangle(cornerRadius: 16) .fill(.fill.tertiary) .frame(height: 140) .overlay { ProgressView("Loading photos") .controlSize(.large) .padding(20) .background(.regularMaterial, in: .rect(cornerRadius: 14)) } ``` ## Related `gauge`, `slider`, `async-image` --- id: gauge title: Gauge platforms: [iOS, iPadOS] availability: iOS 16.0+, macOS 13.0+, watchOS 7.0+, visionOS 1.0+ docs: https://developer.apple.com/documentation/swiftui/gauge.md hig: https://developer.apple.com/design/human-interface-guidelines/gauges.md --- # Gauge Shows where a value sits within a range, like a battery level or a speedometer. Also called: meter, , ring, progress ring, dial, speedometer, battery, level indicator, donut. ## Contents - Default code - Rules - Key APIs - Variants - Related ## Default code ```swift Gauge(value: 21, in: 0...40) { Text("Temperature") } currentValueLabel: { Text("21°") } .gaugeStyle(.accessoryCircular) ``` **Note:** The `accessory` styles are sized for widgets and the Lock Screen. On an app screen, keep them next to text or enlarge them with `.scaleEffect(_:)`. ## Rules - **Label the value and both ends.** Short labels tell people what the range means, and VoiceOver reads them for people who can't see the gauge. - **Use a gradient when the color carries meaning.** Blue to red for cold to hot, or green to purple for air quality, tells people whether a value is good before they read it. - **Use a gauge to show a value, not to set one.** People can't drag a gauge. When they should change the value, use a Slider (`slider`). ## Key APIs - `Gauge(value:in:label:)`: Shows a value in a range; `0...1` by default. - `currentValueLabel:`: The value as text, often in the middle of a ring. - `minimumValueLabel:` / `maximumValueLabel:`: Labels for the two ends. - `.gaugeStyle(_:)`: A bar, a ring, or a widget-style look. - `.tint(_:)`: A color, or a `Gradient` across the range. ## Variants ### Battery ring Use the `.accessoryCircularCapacity` style for a ring that fills, with the value in the middle. ```swift @State private var battery = 0.8 VStack(spacing: 20) { Gauge(value: battery) { Image(systemName: "iphone") } currentValueLabel: { Text(battery.formatted(.percent.precision(.fractionLength(0)))) } .gaugeStyle(.accessoryCircularCapacity) .tint(battery < 0.2 ? .red : .green) .scaleEffect(1.4) Slider(value: $battery) } ``` **Note:** This is the filling ring people often expect from `ProgressView`. On iOS, a circular `ProgressView` only spins. ### Storage bar Use `.gaugeStyle(.linearCapacity)` for a labeled bar that shows usage, like storage or a data plan. ```swift @State private var storage = 182.0 Gauge(value: storage, in: 0...256) { Text("iPhone Storage") } currentValueLabel: { Text("\(Int(storage)) GB of 256 GB used") } .gaugeStyle(.linearCapacity) .tint(.orange) ``` ### Air quality scale Use `.accessoryLinear` with a `Gradient` tint, so the marker's color shows whether a value is good. ```swift Gauge(value: 42, in: 0...200) { Text("AQI") } currentValueLabel: { Text("42") } minimumValueLabel: { Text("Good") } maximumValueLabel: { Text("Poor") } .gaugeStyle(.accessoryLinear) .tint(Gradient(colors: [.green, .yellow, .orange, .red, .purple])) ``` ## Related `progress-view`, `slider`, `charts` --- id: link title: Link platforms: [iOS, iPadOS, macOS] availability: iOS 14.0+, macOS 11.0+, visionOS 1.0+ docs: https://developer.apple.com/documentation/swiftui/link.md --- # Link A control that opens a URL, in Safari or in the app that handles it. ## Contents - Default code - Pitfalls - Key APIs - Variants - On macOS - Related ## Default code ```swift Link("Terms of Service", destination: URL(string: "https://www.apple.com/legal/")!) // Or inline, with Markdown Text("Read our [Privacy Policy](https://www.apple.com/privacy/).") ``` **Important:** Markdown links only work in string literals. A `String` variable shows the brackets as typed; use `Text(LocalizedStringKey(string))` instead. ## Pitfalls - **Web links leave your app.** They open in Safari. Other schemes open their own app: `mailto:` opens Mail, `tel:` the phone, and universal links the matching app. - **A `Link` is a button.** `.buttonStyle(_:)`, `.tint(_:)`, and `.font(_:)` work on it like on any Button (`button`). ## Key APIs `Link(_:destination:)`, `Link(destination:label:)`, `Text("[title](url)")`, `@Environment(\.openURL)`, `prefersInApp:`, `OpenURLAction` ## Variants ### Links inside text Write Markdown links in a `Text` literal for inline links, and color them with `.tint(_:)`. ```swift Text("By continuing you agree to our [Terms](https://www.apple.com/legal/) and [Privacy Policy](https://www.apple.com/privacy/).") .font(.footnote) .multilineTextAlignment(.center) .tint(.purple) ``` ### Open a URL from code Read `openURL` from the environment to open a link from any action, like a button or list row. ```swift @Environment(\.openURL) private var openURL Button("Visit swift.org", systemImage: "safari") { if let url = URL(string: "https://www.swift.org") { openURL(url) } } .buttonStyle(.bordered) ``` **Note:** On iOS 26, call `openURL(url, prefersInApp: true)` to show a web page in a Safari sheet without leaving your app. ### Intercept links Set an `OpenURLAction` in the environment to handle every link tap below it yourself. ```swift @State private var lastTapped = "Nothing yet" VStack { Text("Open [Settings](myapp://settings) or [Help](myapp://help)") Text("Tapped: \(lastTapped)") .foregroundStyle(.secondary) } .environment(\.openURL, OpenURLAction { url in lastTapped = url.host() ?? url.absoluteString return .handled }) ``` **Note:** Return `.handled` to stay in the app, or `.systemAction` to open the link normally. Try it: nothing here leaves the app. ### Email and phone Use `mailto:` and `tel:` URLs to start an email in Mail or a call in Phone. ```swift HStack { Link(destination: URL(string: "mailto:hello@example.com")!) { Label("Email", systemImage: "envelope") } Link(destination: URL(string: "tel:+15555550123")!) { Label("Call", systemImage: "phone") } } .buttonStyle(.bordered) ``` **Note:** The Simulator has no Mail or Phone app, so test these on a device. ## On macOS Text that opens a web page in the person's default browser when clicked. ### Default code ```swift Link("Privacy Policy", destination: URL(string: "https://www.apple.com/privacy/")!) ``` **Note:** Like an ``, but the page opens in the default browser, outside your app. Use a Button (`button`) for actions that stay inside. ### Pitfalls - **Universal Links open their app.** For a link that belongs to an installed app, the system opens it there; everything else goes to the default browser. - **Markdown links in `Text` work too.** A string literal like `"See [our site](https://…)"` becomes a clickable link, and it goes through the same `openURL` action. ### Key APIs `OpenURLAction { url in … }`, `.environment(\.openURL, …)` ### Variants #### A link inside a sentence Write the link in Markdown inside a `Text` to link a few words in the middle of a sentence. ```swift Text("By continuing, you agree to the [Privacy Policy](https://www.apple.com/privacy/).") ``` #### A link with a symbol `Link(destination:label:)` takes any view as its label, such as a `Label` with an SF Symbol. ```swift Link(destination: URL(string: "https://developer.apple.com/swift/")!) { Label("Learn Swift", systemImage: "swift") } ``` #### Open a URL from code Read `openURL` from the environment when a button needs to do something first, then open the page. ```swift @Environment(\.openURL) private var openURL Button("Open Website", systemImage: "safari") { logVisit() openURL(URL(string: "https://www.apple.com/")!) } ``` #### Handle links yourself Set your own `OpenURLAction` to catch links before the browser does, for example to open a help topic inside the app. ```swift Text("Stuck? Read [Getting Started](help:getting-started) or [Shortcuts](help:shortcuts).") .environment(\.openURL, OpenURLAction { url in guard url.scheme == "help" else { return .systemAction } showHelpTopic(url.absoluteString) return .handled }) ``` **Note:** Return `.systemAction` for links you don't handle, so they still open normally. ## Related `button`, `share-link`, `text`, `text-label`, `copy-paste` --- id: share-link title: ShareLink platforms: [iOS, iPadOS, macOS] availability: iOS 16.0+, macOS 13.0+, visionOS 1.0+ docs: https://developer.apple.com/documentation/swiftui/sharelink.md hig: https://developer.apple.com/design/human-interface-guidelines/activity-views.md --- # ShareLink A button that opens the share sheet to send a link, text, or image. ## Contents - Default code - Rules - Pitfalls - Key APIs - Variants - On macOS - Related ## Default code ```swift let article = URL(string: "https://www.swift.org")! ShareLink(item: article) ShareLink(item: article) { Label("Share Article", systemImage: "square.and.arrow.up") } ``` **Important:** Only `URL` and `String` items work without a preview. For an `Image` or your own type, also pass a `preview:` made with `SharePreview`. ## Rules - **Use the standard Share symbol.** People look for `square.and.arrow.up` when they want to send something. A different icon makes them hunt for it. - **Don't rebuild actions the share sheet already has.** Copy, Print, AirDrop, and Save to Files are built in. Your own versions would sit next to them and confuse people. - **Give shared items a clear preview.** A `SharePreview` with a title and image shows people what they're about to send at the top of the sheet. ## Pitfalls - **You can share anything `Transferable`.** `URL`, `String`, `Image`, and `Data` work out of the box; make your own types conform to share them too. ## Key APIs `ShareLink(item:)`, `ShareLink(item:preview:)`, `SharePreview(_:image:)`, `subject:` / `message:`, `ShareLink(items:)`, `label:` ## Variants ### Share button in a toolbar Put a `ShareLink` in `.toolbar` on a detail screen, where people look for sharing first. ```swift NavigationStack { Text("Article") .navigationTitle("Article") .toolbar { ShareLink(item: URL(string: "https://www.swift.org")!) } } ``` **Note:** In a toolbar, `ShareLink` shows only its icon. It's the same button people see in Safari and Photos. ### Share an image Pass an `Image` as `item:` with a `SharePreview` that names it and shows a thumbnail in the sheet. ```swift let photo = Image(systemName: "mountain.2.fill") VStack { photo .font(.system(size: 60)) .foregroundStyle(.teal) ShareLink( item: photo, preview: SharePreview("Mountains", image: photo) ) { Label("Share Photo", systemImage: "photo") } .buttonStyle(.bordered) } ``` ## On macOS A Share button that sends a link, text, or your own data to other apps. ### Default code ```swift ShareLink(item: URL(string: "https://developer.apple.com/swiftui/")!) ``` **Note:** `ShareLink` makes the button and shows the system's sharing services when people click it. You only supply what to share. ### Rules - **Use the standard Share button.** People know its symbol and what it does. `ShareLink` gives you that button, so don't build another way to reach the same services. - **Put Share where Mac users look for it.** On the Mac, people find sharing in a window's toolbar and in the context menu of the item they want to share. - **Share the richest data you have.** List your type's representations from most to least specific. Each service picks the best one it understands. ### Pitfalls - **Some services want a file.** Mail, Messages, AirDrop, and Notes may not appear for every data type. If one is missing, add a `FileRepresentation` to your type. - **A preview skips the wait.** For a URL, the system fetches the page's title and icon over the network. A `SharePreview` you supply shows right away. ### Key APIs `ShareLink(_:item:)`, `subject:`, `message:`, `Transferable` ### Variants #### In the toolbar Put a `ShareLink` in the toolbar for whatever the window shows, icon-only with a `.help` tooltip. ```swift .toolbar { ShareLink(item: photo, preview: SharePreview("Sunset", image: photo)) .help("Share this photo") } ``` **Note:** A real `.toolbar` here would land in this window's own toolbar, so the preview draws one. The Share button is real. #### In a context menu A `ShareLink` works as a context menu item too, so people can share the item they Control-click. ```swift NoteCard(note) .contextMenu { Button("Duplicate") { duplicate(note) } ShareLink(item: note.text) } ``` #### Share your own type Make your type `Transferable`, like this tip, which shares as plain text with a title in the preview. ```swift struct GardenTip: Transferable { let title: String let body: String static var transferRepresentation: some TransferRepresentation { ProxyRepresentation { tip in "\(tip.title)\n\n\(tip.body)" } } } ShareLink(item: tip, preview: SharePreview(tip.title, image: Image(systemName: "leaf"))) ``` More variants, left out to keep this file short: Share several items. Apple's docs: https://developer.apple.com/documentation/swiftui/sharelink.md ## Related `link`, `button`, `toolbar`, `image`, `drag-drop`, `copy-paste`, `file-import-export`, `context-menu` --- id: text-field title: TextField platforms: [iOS, iPadOS, macOS] availability: iOS 13.0+, macOS 10.15+, visionOS 1.0+ docs: https://developer.apple.com/documentation/swiftui/textfield.md hig: https://developer.apple.com/design/human-interface-guidelines/text-fields.md --- # TextField A box people type a short piece of text or a number into. ## Contents - Default code - Rules - Pitfalls - Key APIs - Variants - On macOS - Related ## Default code ```swift @State private var name = "" TextField("Name", text: $name) .textFieldStyle(.roundedBorder) ``` **Important:** Outside a `Form` or `List`, the default style has no border, so the field can look invisible. Add `.textFieldStyle(.roundedBorder)` or a background of your own. ## Rules - **Keep a visible label once people start typing.** The placeholder disappears with the first keystroke. A label beside or above the field still says what each value is. - **Show the keyboard that fits the content.** An email field with `.emailAddress` puts @ on the first screen, and a price with `.decimalPad` shows only digits. It saves taps and typos. - **Validate at the right moment.** Check an email address when people leave the field, not on the first letter. Show the problem right under the field, in words. - **Size the field to the text you expect.** A short field for a ZIP code and a wide one for a street address tell people how much to type. ## Pitfalls - **The field edits a string you own.** Pass a `@State` property with `$`, like a controlled `` in React. It updates on every keystroke. - **Let it grow for longer text.** `TextField(_:text:axis: .vertical)` wraps onto new lines as people type; cap it with `.lineLimit(1...5)`. For a full page of text, use TextEditor (`text-editor`). - **Focus and the keyboard are state too.** To move between fields on Return or hide the keyboard, see Keyboard & Focus (`focus-state`). ## Key APIs `TextField(_:text:)`, `TextField(_:value:format:)`, `axis: .vertical`, `.textFieldStyle(_:)`, `.keyboardType(_:)`, `.textContentType(_:)`, `.autocorrectionDisabled()`, `.onSubmit { }` ## Variants ### In a Form Inside a `Form`, fields need no style. Wrap one in `LabeledContent` to show a label beside it. ```swift @State private var name = "Taylor" @State private var city = "" Form { TextField("Name", text: $name) LabeledContent("City") { TextField("Required", text: $city) .multilineTextAlignment(.trailing) } } ``` ## On macOS A single-line field for short text, like a name, an email address, or a number. ### Default code ```swift @State private var name = "" TextField("Name", text: $name, prompt: Text("Jane Appleseed")) .onSubmit { print("Hello, \(name)") } ``` **Note:** The binding updates on every keystroke, like a controlled ``. `onSubmit` runs when people press Return. ### Rules - **Size the field for what goes in it.** The width hints at how much to type. A ZIP code needs a short field; a street address needs a long one. - **Keep a label next to the field.** Placeholder text disappears as soon as people type, so a label beside the field keeps reminding them what it's for. - **Check input at the right moment.** Check an email address when people move to the next field. Check a new user name before they move on, so they can fix it right away. - **Let Tab move through fields in order.** People expect focus to go top to bottom and leading to trailing. SwiftUI does this for you; see Focus & keyboard navigation (`focus-keyboard`) when you need more. ### Pitfalls - **A `String` updates as people type; other values wait.** A field bound with `value:format:` changes its value only when people commit the edit, such as by pressing Return. - **On macOS 27 and later, use `.bordered` for a border.** It replaces `.roundedBorder`; pair it with `.textInputBorderShape(_:)`, such as `.capsule`, to choose the shape. ### Key APIs `TextField(_:text:prompt:)`, `TextField(_:text:axis:)`, `.textInputSuggestions { }` ### Variants #### Labels in a form In a `Form`, the Mac puts each field's label at the leading edge and shows the `prompt` inside the field. ```swift @State private var name = "" @State private var email = "" Form { TextField(text: $name, prompt: Text("Required")) { Text("Name") } TextField(text: $email, prompt: Text("Optional")) { Text("Email") } } ``` More variants, left out to keep this file short: Email with validation, Numbers and currency, Search field with clear button, Add an item with Return (macOS), A price with a format (macOS), A field that grows (macOS), Suggestions as people type (macOS). Apple's docs: https://developer.apple.com/documentation/swiftui/textfield.md ## Related `secure-field`, `text-editor`, `focus-state`, `form`, `focus-keyboard`, `labeled-content`, `search` --- id: secure-field title: SecureField platforms: [iOS, iPadOS, macOS] availability: iOS 13.0+, macOS 10.15+, visionOS 1.0+ docs: https://developer.apple.com/documentation/swiftui/securefield.md hig: https://developer.apple.com/design/human-interface-guidelines/text-fields.md --- # SecureField A text field that hides what people type, for passwords and PINs. ## Contents - Default code - Rules - Pitfalls - Key APIs - Variants - On macOS - Related ## Default code ```swift @State private var password = "" SecureField("Password", text: $password) .textContentType(.password) .textFieldStyle(.roundedBorder) ``` **Note:** Use `.password` on a sign-in screen and `.newPassword` on a sign-up screen, so iOS fills in the saved password or suggests a strong one. ## Rules - **Always hide passwords and other secrets.** Someone nearby can read a plain field over a shoulder or in a screen recording. A `SecureField` shows dots instead. - **Support AutoFill.** With the right `.textContentType`, people sign in with Face ID instead of typing. Mark the username field too, so both fill at once. - **Check a new password before people move on.** Show the rules and whether they're met while people type. Finding out after Submit means typing it all again. ## Pitfalls - **It works like a `TextField`.** It takes the same styles and modifiers, and the string you bind holds the real text. Only the display is hidden. - **iOS locks it down for you.** Autocorrect, dictation, and copying are off, and each character shows for a moment before it turns into a dot. ## Key APIs `SecureField(_:text:)`, `.textContentType(.password)`, `.textContentType(.newPassword)`, `.textContentType(.oneTimeCode)`, `.textFieldStyle(_:)`, `.onSubmit { }` ## Variants ### Sign-in form In a `Form`, fields need no style. Mark them `.username` and `.password` so AutoFill fills both. ```swift @State private var username = "" @State private var password = "" Form { TextField("Username", text: $username) .textContentType(.username) .textInputAutocapitalization(.never) SecureField("Password", text: $password) .textContentType(.password) Button("Sign In") { // Sign in } .disabled(username.isEmpty || password.isEmpty) } ``` ## On macOS A text field that hides what people type, for passwords and other secrets. ### Default code ```swift @State private var password = "" SecureField("Password", text: $password) .textContentType(.password) .onSubmit { print("Signing in…") } ``` **Note:** Like `TextField`, the binding updates on every keystroke, so you can check the length or enable a button as people type. ### Rules - **Never fill in a password for people.** Ask people to type it each time, or use biometric or keychain authentication instead. - **Turn on Sign In only when it can work.** Keep the button disabled until people fill in the required fields, so it's clear what's missing. - **Use a secure field for anything sensitive.** Passwords, PINs, and recovery codes belong in a `SecureField`, never a plain `TextField`. ### Pitfalls - **The field protects its text for you.** It shows a dot for each character, won't let people cut or copy the contents, and shows an indicator while Caps Lock is on. - **Consider not asking for a password at all.** The HIG suggests Sign in with Apple, or passkeys, before an app-specific password. ### Key APIs `SecureField(_:text:prompt:)`, `.textContentType(.username)`, `.keyboardShortcut(.defaultAction)` ### Variants #### A sign-in form Mark the fields with `.textContentType` so the system can suggest saved accounts, and give Sign In `.keyboardShortcut(.defaultAction)` so Return clicks it. ```swift @State private var username = "" @State private var password = "" @State private var message = "" Form { TextField("User Name", text: $username) .textContentType(.username) SecureField("Password", text: $password) .textContentType(.password) Button("Sign In") { message = "Signed in as \(username)" } .keyboardShortcut(.defaultAction) .disabled(username.isEmpty || password.isEmpty) Text(message) .foregroundStyle(.secondary) } .frame(width: 320) ``` **Note:** On the Mac, the default button gets special coloring, so people can see that Return clicks it. #### Create a password Use `.textContentType(.newPassword)` for a password people are making, and check the two fields match as they type. ```swift @State private var newPassword = "" @State private var confirmation = "" Form { SecureField("New Password", text: $newPassword) .textContentType(.newPassword) SecureField("Verify", text: $confirmation) .textContentType(.newPassword) if !confirmation.isEmpty && confirmation != newPassword { Text("The passwords don't match.") .foregroundStyle(.red) } } .frame(width: 320) ``` More variants, left out to keep this file short: Show / hide password, New password with strength meter, Show the password (macOS). Apple's docs: https://developer.apple.com/documentation/swiftui/securefield.md ## Related `text-field`, `focus-state`, `login-form`, `focus-keyboard`, `form`, `keyboard-shortcuts` --- id: text-editor title: TextEditor platforms: [iOS, iPadOS, macOS] availability: iOS 14.0+, macOS 11.0+, visionOS 1.0+ docs: https://developer.apple.com/documentation/swiftui/texteditor.md hig: https://developer.apple.com/design/human-interface-guidelines/text-views.md --- # TextEditor A scrolling, multi-line area for long text such as notes or a bio. ## Contents - Default code - Rules - Pitfalls - Key APIs - Variants - On macOS - Related ## Default code ```swift @State private var notes = "" TextEditor(text: $notes) .frame(height: 150) ``` **Important:** A `TextEditor` takes all the space it's offered. Outside a `Form`, give it a `.frame(height:)` or `minHeight`. ## Rules - **Use a text editor for long text, and a text field for short text.** A bio or a note needs room to see several lines at once. A one-line field hides most of what people wrote. - **Keep the text readable at every size.** Use a text style like `.body` rather than a fixed point size, so the editor follows Dynamic Type. - **Tell people what to write when it's empty.** An empty box gives no hint. Add a placeholder, and a counter if there's a limit. ## Pitfalls - **Hide the default background before adding your own.** `.background` alone has no effect until you add `.scrollContentBackground(.hidden)`. - **Text modifiers style all of the text.** `.font`, `.foregroundStyle`, and `.lineSpacing` apply to the whole editor. For styled runs, bind an `AttributedString`. ## Key APIs `TextEditor(text:)`, `.frame(height:)`, `.scrollContentBackground(.hidden)`, `.font(_:)`, `.lineSpacing(_:)`, `.onChange(of:)` ## Variants ### Placeholder `TextEditor` has no placeholder, so overlay a `Text` and hide it once there's text. ```swift @State private var notes = "" TextEditor(text: $notes) .overlay(alignment: .topLeading) { if notes.isEmpty { Text("Write something…") .foregroundStyle(.tertiary) .padding(.top, 8) .padding(.leading, 5) .allowsHitTesting(false) } } .frame(height: 120) ``` **Note:** The padding lines the placeholder up with the editor's first line, and `.allowsHitTesting(false)` lets taps reach the editor. ### Character limit Trim the text in `.onChange(of:)` to enforce a maximum, and show a counter below the editor. ```swift @State private var bio = "" let limit = 80 VStack(alignment: .trailing) { TextEditor(text: $bio) .frame(height: 100) .onChange(of: bio) { if bio.count > limit { bio = String(bio.prefix(limit)) } } Text("\(bio.count)/\(limit)") .font(.caption.monospacedDigit()) .foregroundStyle(bio.count == limit ? .red : .secondary) } ``` ## On macOS A scrolling area for long, multi-line text, plain or styled. ### Default code ```swift @State private var notes = "" TextEditor(text: $notes) .frame(minHeight: 120) ``` **Note:** A `TextEditor` scrolls when the text is longer than its frame, like a `