In this tutorial we’re going to learn how to build a horizontal, linear progress bar indicator in SwiftUI. The sample app will include two buttons – one will simulate the progress and will keep updating the progress bar with new value, and the other will simply reset the progress bar to its initial state. Start by creating new single view iOS app using SwiftUI.

Design/Architecture considerations
- Just like in a UIProgressView, our current progress value will be represented by a Float which can have values between 0.0 and 1.0 (inclusive). It will simplify math later on in the tutorial.
- If you set the current progress value to anything below 0.0 or above 1.0, the progress bar will treat it as 0.0 or 1.0 respectively and won’t extend beyond its bounds.
- Our progress bar will adapt to its container/parent view in terms of width and will take up all space available. You (as a developer) will be able to control the height (and width, if desired) of the progress bar by using frame() modifier.
- The container/parent view will manage the state of progress bar (e.g., its current value).
Start building ProgressBar view
Start by creating empty ProgressBar view with a reference to its current value binding and one rectangle reflecting the progress bar. Wrap the Rectangle view in a container view (GeometryReader) in order to adjust ProgressBar to the size of its parent view (by being able to access the geometry variable which references size and coordinate space of container view).
struct ProgressBar: View {
@Binding var value: Float
var body: some View {
GeometryReader { geometry in
Rectangle().frame(width: geometry.size.width , height: geometry.size.height)
}
}
}
Try using the newly created ProgressBar view in ContentView by setting its height to desired value and letting the bar spread across entire available width. Take a note that we added a new State variable to manage the current progress value and we’re passing the binding to ProgressBar view. The result does not look good yet because it lacks proper styling and adjustments.
struct ContentView: View {
@State var progressValue: Float = 0.0
var body: some View {
VStack {
ProgressBar(value: $progressValue).frame(height: 20)
Spacer()
}.padding()
}
}
Wrapping the ProgressBar in a VStack allowed us to push ProgressBar view to the top of the screen by adding a Spacer and applying standard padding to it.

Apply proper styling to ProgressBar
Let’s focus on styling our Rectangle to make it look like the desired progress bar. First, wrap it in a ZStack, round its corners and then change the color and opacity of underlying Rectangle.
struct ProgressBar: View {
@Binding var value: Float
var body: some View {
GeometryReader { geometry in
ZStack {
Rectangle().frame(width: geometry.size.width , height: geometry.size.height)
.opacity(0.3)
.foregroundColor(Color(UIColor.systemTeal))
}.cornerRadius(45.0)
}
}
}
It looks much better already!

Highlight progress on the ProgressBar
In order to show progress on the new ProgressBar we’re going to overlay a second (darker) Rectangle on top of the current (lighter) one which will create a beautiful impression of having a progress indicator. This is the reason why we used a ZStack in previous section – it will allow us to place one Rectangle on top of the other.
Add a second rectangle to the ZStack and fix its width for now. Let’s see what happens.
ZStack {
Rectangle().frame(width: geometry.size.width , height: geometry.size.height)
.opacity(0.3)
.foregroundColor(Color(UIColor.systemTeal))
Rectangle().frame(width: 60.0, height: geometry.size.height)
.foregroundColor(Color(UIColor.systemBlue))
}.cornerRadius(45.0)

It’s slowly moving towards our goal but we want the ProgressBar to extend from left to right of the screen and to be dependent on current progress value.
In order to align it from left to right we need to apply a leading alignment to the ZStack which wraps it:
ZStack(alignment: .leading)
Remember we decided that our progress value is a Float between 0.0 and 1.0 which means that in order to correctly reflect its width in the parent view, we need to multiply current progress value by the width of parent view. On top of that, we need to make sure to never allow values greater than 1.0 (or in other words, values greater than the width of the parent view). Here is the formula which we’re going to use to calculate the width of progress indicator (replacement of the fixed width set to 60.0 previously):
min(CGFloat(self.value)*geometry.size.width, geometry.size.width)
Let’s also apply an animation to the progress indicator rectangle when its value changes. The ZStack code should now look like this:
ZStack(alignment: .leading) {
Rectangle().frame(width: geometry.size.width , height: geometry.size.height)
.opacity(0.3)
.foregroundColor(Color(UIColor.systemTeal))
Rectangle().frame(width: min(CGFloat(self.value)*geometry.size.width, geometry.size.width), height: geometry.size.height)
.foregroundColor(Color(UIColor.systemBlue))
.animation(.linear)
}.cornerRadius(45.0)
If you navigate to ContentView and update its progressValue to 0.2, then the progress bar will look like this:
struct ContentView: View {
@State var progressValue: Float = 0.2

Connect ProgressBar to code which updates it
Our ProgressBar view is complete at this point. Now add two buttons to the ContentView which will control our progress bar. One button will start the progress simulation by updating its progress value, and the other button will reset the progress bar to its initial state after the progress simulation completes.
The complete code for this tutorial should look like this:
import SwiftUI
struct ProgressBar: View {
@Binding var value: Float
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle().frame(width: geometry.size.width , height: geometry.size.height)
.opacity(0.3)
.foregroundColor(Color(UIColor.systemTeal))
Rectangle().frame(width: min(CGFloat(self.value)*geometry.size.width, geometry.size.width), height: geometry.size.height)
.foregroundColor(Color(UIColor.systemBlue))
.animation(.linear)
}.cornerRadius(45.0)
}
}
}
struct ContentView: View {
@State var progressValue: Float = 0.0
var body: some View {
VStack {
ProgressBar(value: $progressValue).frame(height: 20)
Button(action: {
self.startProgressBar()
}) {
Text("Start Progress")
}.padding()
Button(action: {
self.resetProgressBar()
}) {
Text("Reset")
}
Spacer()
}.padding()
}
func startProgressBar() {
for _ in 0...80 {
self.progressValue += 0.015
}
}
func resetProgressBar() {
self.progressValue = 0.0
}
}
Take a look at the final result:

Related tutorials:
- How to add borders to SwiftUI views
- Advanced SwiftUI button styling and animation
- How to expand SwiftUI views to span across entire width or height of screen
Questions, comments or suggestions? Follow us on Twitter @theswiftguide