For any iOS Developer, mastering basic interactive elements is the first step toward mastery. Back in the days of UIKit, configuring a button with an image and text required dealing with content insets, control states (.normal, .highlighted), and a lot of imperative code that often ended up breaking on different screen sizes.
Today, thanks to modern Swift programming, the paradigm has completely shifted. SwiftUI has democratized and simplified interface creation. However, don’t let its apparent simplicity fool you: behind that clean syntax lies an incredibly powerful and nuanced rendering engine.
In this in-depth tutorial, we are going to explore every detail on how to create a button with an image in SwiftUI. From using Apple’s native iconography to implementing custom cross-platform styles that work flawlessly on iOS, macOS, and watchOS directly from Xcode.
1. The Anatomy of a Button in SwiftUI
Before throwing images onto the screen, as a good iOS Developer, you must understand how SwiftUI thinks. In this framework, a Button is not a monolithic class with predefined properties; it is a container view that clearly separates two things:
- The Action: The block of Swift code that executes when the user interacts with the control.
- The Label: The view (or group of views) that defines the visual appearance of the button.
This separation is brilliant because it means the Label can be literally anything: text, an image, a complex gradient, or a combination of several elements.
2. The Gold Standard: Creating a Button with SF Symbols
The fastest, most efficient, and Apple-recommended way to create a button with an image in SwiftUI is by using SF Symbols.
SF Symbols is the vector iconography library integrated into all Apple operating systems. Because they are based on vectors and typography, these icons scale perfectly, support different weights, and magically align with text.
Open Xcode, create a new view, and see how simple it is:
import SwiftUI
struct SFSymbolButtonView: View {
var body: some View {
Button(action: {
print("Star button pressed!")
}) {
Image(systemName: "star.fill")
.font(.system(size: 40))
.foregroundColor(.yellow)
.shadow(radius: 5)
}
}
}
Why do we use .font on an Image?
In Swift programming with SwiftUI, images created with systemName behave like text. This means you can use typography modifiers like .font(), .fontWeight(), or .imageScale() to adjust their size without losing resolution, guaranteeing perfect accessibility if the user relies on Dynamic Type (large text sizes).
3. Using Your Own Images (Xcode Assets)
Sometimes, SF Symbols are not enough. Your design team might hand you a custom logo or icon in PNG or SVG format. To create a button with an image in SwiftUI using your own resources, you must first drag the image into your Assets catalog in Xcode.
Let’s assume you’ve added an icon named "custom_logo".
import SwiftUI
struct AssetButtonView: View {
var body: some View {
Button(action: {
print("Logo pressed")
}) {
Image("custom_logo")
.renderingMode(.template) // 1. Allows tinting the image
.resizable() // 2. Allows resizing
.scaledToFit() // 3. Maintains aspect ratio
.frame(width: 60, height: 60)
.foregroundColor(.blue) // 4. Applies the color
}
}
}
The Secret of the Rendering Mode
As an iOS Developer, you must know the .renderingMode() modifier.
- If you use
.original, SwiftUI will draw the image with its original full colors. The.foregroundColorwill be ignored. - If you use
.template, SwiftUI will treat the image as an alpha mask (a stencil), ignoring its original colors and tinting it completely with the color you pass in.foregroundColor()or.tint(). This is essential for adaptive icons (Light/Dark Mode).
4. Combining Image and Text: The Power of Label
Rarely is a button just an isolated image without context. The Human Interface Guidelines suggest that unless the icon is universally understood (like a magnifying glass for search), it’s always better to accompany it with text.
Instead of manually building an HStack with an Image and a Text, SwiftUI offers us the Label view. It is semantically correct, optimized for accessibility, and adjusts spacing automatically.
import SwiftUI
struct IconTextButtonView: View {
var body: some View {
Button(action: {
print("Added to favorites")
}) {
Label("Add to Favorites", systemImage: "heart.fill")
.font(.headline)
.padding()
.foregroundColor(.white)
.background(Color.pink)
.clipShape(Capsule())
}
}
}
With just a few lines of Swift code, we have created a pill-shaped button that looks amazing and is fully interactive.
5. Cross-Platform Design: iOS, macOS, and watchOS
The true promise of SwiftUI is “Learn once, apply anywhere”. However, as an experienced iOS Developer, you know that the user experience of a Mac is not the same as that of an Apple Watch.
When you create a button with an image in SwiftUI, the framework automatically applies native styles depending on where the code is compiled in Xcode.
Considerations for macOS
On the Mac, users interact with a mouse or trackpad. Buttons need Hover states (when the cursor passes over them) and Focus Rings for keyboard navigation.
Button(action: {}) {
Image(systemName: "trash")
}
.buttonStyle(.bordered) // On macOS, this creates a standard native button
.controlSize(.large)
On macOS, using .buttonStyle(.bordered) automatically draws the classic Mac button bezel and handles clicks with precise system animation.
Considerations for watchOS
On the Apple Watch, space is critical, and interaction is touch-based. Touch Targets must be large, at least 44×44 points.
Button(action: {}) {
Image(systemName: "play.circle.fill")
.font(.system(size: 50))
}
.buttonStyle(.plain) // Prevents the default gray background on watchOS
On watchOS, a button often spans the entire width of the screen or is presented as a massive floating circle. Make sure your image scales appropriately.
6. Elevating the Design: Custom ButtonStyle
When you modify the .background or .foregroundColor of a traditional button, you often lose the default “pressed” animation (the dimming or fading of the button when you touch it).
To build production-grade buttons in Swift programming, the ultimate technique is to create your own ButtonStyle. This gives you absolute control over how the button reacts when it is pressed (isPressed).
Let’s create a style that makes our image “pulse” (shrink) when tapped, a very popular effect in iOS apps.
import SwiftUI
// 1. We define our custom style
struct PulseImageButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding(15)
.background(
Circle()
.fill(configuration.isPressed ? Color.blue.opacity(0.7) : Color.blue)
)
.foregroundColor(.white)
// 2. We control the scale based on the isPressed state
.scaleEffect(configuration.isPressed ? 0.85 : 1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.6), value: configuration.isPressed)
}
}
// 3. We use it in our view
struct CustomStyleView: View {
var body: some View {
Button(action: {
print("Camera activated")
}) {
Image(systemName: "camera.fill")
.font(.title)
}
// We apply our new style
.buttonStyle(PulseImageButtonStyle())
}
}
By encapsulating the design in a ButtonStyle, you keep your main view clean and can reuse this exact interactive visual style throughout your application, adhering to the DRY (Don’t Repeat Yourself) principle of good Swift programming.
7. Accessibility: The Duty of Every iOS Developer
An aspect you should never overlook when you create a button with an image in SwiftUI is accessibility (VoiceOver).
If you use a Label with text and an image, SwiftUI is smart enough to read the text. But what if your button only has an image (like our star or camera example)? VoiceOver might simply say “Star, Button” or, even worse, the image file name.
To ensure your application is usable by everyone, you must add an explicit accessibility label:
Button(action: {
// Send action
}) {
Image(systemName: "paperplane.fill")
.font(.title)
}
.accessibilityLabel("Send message") // 1. What VoiceOver will read
.accessibilityHint("Sends the message currently typed in the chat.") // 2. Optional extra context
With these two simple lines, you transform a frustrating experience for a visually impaired user into a top-tier, empathetic, professional application.
Conclusion
The art of knowing how to create a button with an image in SwiftUI goes far beyond simply putting an icon on the screen. As we’ve seen in this journey through Swift programming, it involves understanding image rendering modes, the semantic power of the Label view, cross-platform adaptability, and the creation of custom styles using the ButtonStyle protocol.
SwiftUI provides you with an extraordinarily flexible canvas inside Xcode. Whether you are building a dense dashboard for macOS, a fast touch interface for watchOS, or a fluid application for iOS, these fundamentals will ensure your controls are visually appealing, highly interactive, and completely accessible.
If you have any questions about this article, please contact me and I will be happy to help you . You can contact me on my X profile or on my Instagram profile.