If you are an iOS Developer looking to perfect the user interface of your applications, you have surely encountered the challenge of adapting native components to specific design requirements. One of the most commonly used elements to indicate that a task is in progress is the ProgressView. However, during the early days of SwiftUI, modifying its dimensions wasn’t quite as intuitive.
Today, changing the size of ProgressView in SwiftUI is an elegant and semantic process thanks to the controlSize() modifier. In this tutorial, we will explore in depth how to use this tool in Swift programming to create adaptable and consistent interfaces across iOS, macOS, and watchOS, all from Xcode.
What is ProgressView in SwiftUI?
Before diving into sizes, it is essential to understand what this component represents. In Swift, a ProgressView is a view that shows the progress of a task. It can be presented in two main ways:
- Indeterminate (Spinner): Displays a circular animation (commonly known as an “activity indicator”) when it is unknown how long a task will take.
- Determinate (Progress Bar): Displays a linear bar that fills from 0.0 to 1.0 (or any other range) when the progress of the task is calculable.
Regardless of the type of progress view you use, maintaining a clear visual hierarchy is vital. An indicator that is too small can go unnoticed, while one that is too large can look clunky and ruin the design.
The Problem: scaleEffect() vs frame()
Historically, developers tried to change the size of ProgressView in SwiftUI using generic modifiers like .frame(width:height:) or .scaleEffect().
- The problem with
.frame(): If you apply a larger frame to an indeterminate circularProgressView, the container grows, but the spinner itself keeps its native size in the center. - The problem with
.scaleEffect(): Although it makes the component look larger or smaller through a matrix transformation, it can cause pixels to look blurry, alter the margins of adjacent components unpredictably, and does not respect the operating system’s accessibility guidelines.
This is where the native semantic modifier shines.
The Elegant Solution: controlSize()
Introduced to unify the way we size controls, controlSize() is a modifier that tells SwiftUI what the semantic size of a control should be. Instead of passing exact pixels, you give the system an intention, and SwiftUI takes care of rendering the perfect size depending on whether you are on iOS, macOS, or watchOS.
Available Sizes
The modifier accepts a value from the ControlSize enum. Here are the available values and their general purpose:
| Value | Description |
|---|---|
.mini | The smallest size. Useful for dense lists or heavily saturated toolbars (mostly on macOS). |
.small | Small, ideal for accompanying secondary text or compact cells. |
.regular | The default size. It is the standard you see if you don’t apply any modifiers. |
.large | Large. Perfect for full-screen loading states. |
.extraLarge | The maximum size (available since iOS 17/macOS 14). Useful for empty screens, watchOS interfaces, or main loading modals. |
Practical Implementation in Xcode
Next, we are going to write some real Swift programming to see how this modifier behaves. Open Xcode, create a new multiplatform project (or just an iOS project), and copy the following code.
1. Indeterminate ProgressView (Spinner)
The most common use case is a loading spinner that appears while we make an API request or load data from a database.
import SwiftUI
struct IndeterminateProgressViewExample: View {
var body: some View {
VStack(spacing: 40) {
Text("Indeterminate ProgressView Sizes")
.font(.headline)
HStack(spacing: 30) {
VStack {
ProgressView()
.controlSize(.mini)
Text("Mini").font(.caption)
}
VStack {
ProgressView()
.controlSize(.small)
Text("Small").font(.caption)
}
VStack {
ProgressView()
.controlSize(.regular)
Text("Regular").font(.caption)
}
VStack {
ProgressView()
.controlSize(.large)
Text("Large").font(.caption)
}
if #available(iOS 17.0, macOS 14.0, watchOS 10.0, *) {
VStack {
ProgressView()
.controlSize(.extraLarge)
Text("Extra Large").font(.caption)
}
}
}
}
.padding()
}
}
By running this in your Xcode simulator, you will notice how the system renders perfectly sharp and proportioned spinners for each size, without suffering the distortion that a .scaleEffect() would cause.
2. Determinate ProgressView (Progress Bar)
What happens when we want to change the size of ProgressView in SwiftUI for a progress bar? The controlSize() modifier also affects the thickness (height) of the bar, making it visually adapt to its surroundings.
import SwiftUI
struct DeterminateProgressViewExample: View {
@State private var progressAmount = 0.6
var body: some View {
VStack(spacing: 30) {
Text("Determinate ProgressView Sizes")
.font(.headline)
VStack(alignment: .leading) {
Text("Small")
.font(.caption)
ProgressView(value: progressAmount)
.controlSize(.small)
}
VStack(alignment: .leading) {
Text("Regular")
.font(.caption)
ProgressView(value: progressAmount)
.controlSize(.regular)
}
VStack(alignment: .leading) {
Text("Large")
.font(.caption)
ProgressView(value: progressAmount)
.controlSize(.large)
.tint(.blue) // You can easily change the color
}
}
.padding()
}
}
In the case of progress bars, .controlSize() changes the thickness of the line. If you combine this with a .tint() modifier, you can integrate the bar perfectly into your application’s Design System.
Cross-Platform Development: iOS, macOS, and watchOS
One of the great advantages of being an iOS Developer in Apple’s current ecosystem is that SwiftUI is inherently cross-platform. By using a semantic modifier like controlSize(), you are delegating to the operating system the decision of what exactly “large” or “small” means in terms of pixels.
Behavior on iOS
On the iPhone and iPad, sizes respond very well to touch paradigms. A .large is excellent for the center of the screen when a view is loading for the first time. A .regular or .small is ideal to place inside a button (for example, a “Saving…” button where the spinner temporarily replaces the icon).
Behavior on macOS
Mac applications have a different information density. Users navigate with a cursor, so elements can be smaller. On macOS, .mini and .small are vital for sidebars or lists of items (for example, sync indicators in a mail app). SwiftUI will automatically adjust the millimeters on the Mac screen so that the progress indicator looks like a native AppKit control, not like a stretched iOS app.
Behavior on watchOS
On the Apple Watch, space is the most valuable resource. A circular ProgressView with a .large or .extraLarge can fill almost the entire screen and function as the central visual focus of your application (for example, measuring the progress of a fitness workout). Furthermore, on watchOS, Swift optimizes the animation of the ProgressView to conserve the OLED screen’s battery.
Advanced Use Cases and Best Practices
Knowing how to write the code is only part of the job of an iOS Developer. Knowing when to use it is what separates juniors from seniors. Here are some best practices to master the use of ProgressView.
1. Buttons with Loading States
A very common interface pattern is to show a spinner inside a button when the user taps it, indicating that the action is being processed. For this, .regular is usually too big, breaking the button’s design. The .small size is your best ally.
Button(action: {
// Start network request
}) {
HStack {
Text("Save Profile")
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.controlSize(.small)
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
2. Combined Modifiers
Do not forget that controlSize() works perfectly with other SwiftUI view modifiers. You can apply shadows, opacity, or wrap the spinner in a translucent material to create a modern HUD (Heads-Up Display).
ProgressView("Syncing data...")
.controlSize(.large)
.padding(24)
.background(.regularMaterial) // Translucent glass effect
.cornerRadius(16)
.shadow(color: .black.opacity(0.1), radius: 10, x: 0, y: 5)
3. Accessibility Considerations
When using Swift programming and SwiftUI, much of the accessibility (VoiceOver, Dynamic Type) comes configured out of the box. However, when changing the size of ProgressView in SwiftUI, you must ensure that if you attach a Label to the ProgressView, the font of that label scales correctly with the user’s text preferences. Using controlSize() ensures that the icon itself maintains readable proportions regardless of the user’s Dynamic Type settings.
Conclusion
The evolution of SwiftUI has been marked by the introduction of increasingly declarative and semantic APIs. In previous versions, trying to adjust the size of a spinner or a progress bar could result in design “hacks” that required .scaleEffect and broke the interface’s crispness.
Today, thanks to controlSize(), any iOS Developer can create coherent, accessible, and visually appealing interfaces with a single line of code in Xcode. Whether you are programming for the compact screen of an Apple Watch, adapting a complex view on an iPad, or developing a powerful tool for macOS, trusting Swift‘s semantic intentions ensures that your native components always look exactly as Apple designed them.