How to add Buttons to SwiftUI application | SwiftUI Bootcamp #18
Buttons are how users interact with your app — every tap, submit, and action flows through a Button. After this lesson you'll know all three ways to create buttons in SwiftUI, how to style them to look like any design (text labels, icon circles, capsule outlines), and how they connect to @State to drive UI changes.
What You'll Learn
- Three
Buttoninitializer patterns: simple string label, action+label with text, and action+label with custom shape views - How to style button labels using the same modifier chains you've learned for text and shapes
- How buttons interact with
@Stateto update the UI reactively
Mental Model
Think of a Button in SwiftUI as a transparent interaction wrapper — like a sheet of cling film pressed over any view you want to make tappable. The cling film (the Button) handles the tap detection and calls your action closure. What you see on screen is whatever is inside the cling film — a text label, a circle, an icon, a complex card. SwiftUI separates the behavior (what happens when tapped) from the appearance (what the user sees and taps).
This is why there's no "button style" property — instead, you put any view as the button's label and style it with the same modifiers you already know. A fully custom button is just a custom view inside a Button.
Detailed Explanation
Button has two main forms:
Simple:
Button("Label") { action }— Accepts aStringlabel and an action closure. SwiftUI renders it with the system default button appearance (tinted text). Use this for quick, unstyled buttons in toolbars or alerts.Full:
Button(action: { ... }, label: { ... })— Separates the action closure from the label view builder. The label can be any SwiftUI view — a styledText, aCirclewith an icon, aCapsuleoutline, or a full card.
.accentColor(.red) on a simple button changes the system tint to red. For the full form, use .foregroundColor() inside the label to control text/icon color directly.
.uppercased() is a String method, not a SwiftUI modifier — it transforms the string value before passing it to Text. This is a common pattern for buttons that use ALL CAPS labels per a design spec.
Buttons with @State: the action closure is where you mutate @State variables. When the closure runs, SwiftUI detects the state change and re-renders the affected views. This is the simplest form of interactivity in SwiftUI.
Code Structure
The sample is in ButtonsBootcamp.swift and shows four progressively more customized buttons: a simple tinted label, a styled full-width blue save button, a circular icon button, and a capsule-outlined text button. A @State title string changes on each button press to show the reactive update pattern.
Complete Code
ButtonsBootcamp.swift
import SwiftUI
struct ButtonsBootcamp: View {
@State var title: String = "This is my title" // drives the Text at the top of the screen
var body: some View {
VStack(spacing: 20) {
Text(title) // displays the current @State value — updates reactively on button press
Button("Press me!") { // simplest form: string label + action closure
self.title = "BUTTON WAS PRESSED" // mutating @State triggers a re-render
}
.accentColor(.red) // tints the system-styled button label red
Button(action: {
self.title = "Button #2 was pressed"
}, label: {
Text("Save".uppercased()) // .uppercased() is a String method, not a SwiftUI modifier
.font(.headline)
.fontWeight(.semibold)
.foregroundColor(.white)
.padding()
.padding(.horizontal, 20) // extra horizontal padding for a wide button feel
.background(
Color.blue
.cornerRadius(10)
.shadow(radius: 10) // shadow on the background, not the text
)
})
Button(action: {
self.title = "Button #3"
}, label: {
Circle()
.fill(Color.white)
.frame(width: 75, height: 75) // fixed-size circular button
.shadow(radius: 10)
.overlay(
Image(systemName: "heart.fill")
.font(.largeTitle)
.foregroundColor(Color(#colorLiteral(red: 0.5807225108, green: 0.066734083, blue: 0, alpha: 1))) // deep red-brown heart
)
})
Button(action: {
self.title = "Button #4"
}, label: {
Text("Finish".uppercased())
.font(.caption)
.bold()
.foregroundColor(.gray)
.padding()
.padding(.horizontal, 10)
.background(
Capsule()
.stroke(Color.gray, lineWidth: 2.0) // outline-only capsule style (no fill)
)
})
}
}
}
struct ButtonsBootcamp_Previews: PreviewProvider {
static var previews: some View {
ButtonsBootcamp()
}
}Code Walkthrough
@State var title: String— Declares a state variable owned by this view. SwiftUI re-renders the view whenevertitlechanges. TheText(title)at the top automatically reflects the latest value.Button("Press me!") { self.title = "..." }— The simplest button form.self.titleis needed inside a closure to explicitly reference the struct's property (though in practice,title = ...also works sinceselfis captured automatically). The key is that mutating@Stateinside the closure triggers a re-render..accentColor(.red)— Applies to the system-styled simple button's text color. For the fullaction:label:form, control color inside the label closure instead.- Button #2 with blue background — Uses
action:label:form. The whiteText("SAVE")sits over a blue background applied via.background(Color.blue.cornerRadius(10).shadow(...)). The double.padding()creates a wide, tap-friendly touch target. - Button #3 (circle icon button) — The label is a
Circlewith a heart icon overlaid. The entire circle is tappable because it's inside aButton. Note:Color.white.fill()here is the circle's background, not a colored button style. - Button #4 (capsule outline) — Uses
Capsule().stroke(Color.gray, lineWidth: 2.0)as the background — this creates an unfilled, outlined pill shape. The text is gray and small (.caption), producing a subdued "secondary action" aesthetic.
Common Mistakes
Mistake: Applying .foregroundColor() outside the label closure on a full-form Button When using Button(action:label:), .foregroundColor() applied to the Button itself doesn't override the color inside the label — it affects the system tint. Always set colors inside the label view builder to get predictable, visible results.
Mistake: Making the touch target too small by only wrapping a small icon Apple's Human Interface Guidelines recommend a minimum 44×44pt touch target. A 20pt icon without padding is far too small and leads to missed taps. Add .padding() around the icon inside the button label to expand the hit area, even if the visual icon stays small.
Mistake: Putting side effects (network calls, file writes) directly in the Button action closure Button action closures run on the main thread. Long-running operations block the UI, making it feel frozen. Move side effects to a dedicated function or Task { } block inside the closure. Keep the action closure to state mutations and async task launches.
Key Takeaways
- Use
Button("Label") { action }for simple system-styled buttons; useButton(action:label:)to wrap any custom view as a tappable button - The button's visual appearance is entirely up to the label view — style it with the same modifier chains you use for all other views
- Mutating
@Stateinside the action closure is what drives reactive UI updates — keep the closure focused on state changes
Last updated: June 27, 2026