Rahul Sharma

Creating a Progress Bar in SwiftUI

SwiftUI is Apple’s new powerful framework to build applications across all Apple platforms. It is fast, simple, and easy to learn.

It has automatic support for Dark Mode, Dynamic Type, localization, and accessibility.

What we are going to make

We will build this ProgressBar whose progress can be changed by using the Button. It will also have a beautiful animation.

What we are going to make

Getting Started

Open up Xcode 11 and create a New Xcode Project.

Choose Single View Application under iOS.

Enter whatever you like in the Product Name, and make sure to choose SwiftUI in user interface.

Create new Xcode Project

Click Next. Save it anywhere and click create.

Creating a static Progress Bar

SwiftUI apps don’t have Main.storyboard file. All UI you will make will be done programmatically.

Open up ContentView.swift file. This is the initial view of the app. Make sure the Canvas is shown on the right side.

Now, create a new SwiftUI file and name it ProgressBar. Replace Text() with a ZStack.

SwiftUI has 3 types of stacks. VStack (which stacks the views vertically), HStack (which stacks the views horizontally), and ZStack (which stacks the views on top of each other).

If you add 2 circles, one with radius 200, and other with radius 100, in a ZStack, they will look like 2 concentric circles as they’re added on top of each other rather then added next to each other.

Inside this ZStack, add 2 Rectangle(). One for the progress, and one for the track.

Give the track rectangle the color of gray and opacity of 30%, and the progress rectangle a blue color.

Also, give both the rectangles a fixed frame for now and a corner radius of 4.0 to the ZStack.

ZStack {
    Rectangle()
        .foregroundColor(Color.gray)
        .opacity(0.3)
        .frame(width: 345.0, height: 8.0)
    Rectangle()
        .foregroundColor(Color.blue)
        .frame(width: 200.0, height: 8.0)
}
.cornerRadius(4.0)

ProgressBar would look weird right now. This is because the alignment of ZStack is set to center by default.

Change alignment of ZStack to leading as:

ZStack(alignment: .leading)

Now it should look more like a progress bar.

Changing progress of Progress Bar

Open ProgressBar.swift and add a @Binding property called progress of type CGFloat.

Binding is used to create a two-way connection between a view and its underlying model. Read more on Apple Developer Documentation

@Binding var progress: CGFloat

And change the frame of second rectangle according to progress like:

Rectangle()
	.frame(width: 345.0 * (progress / 100.0), height: 8.0)

Try building and compiler will give you an error

Missing arguement for parameter 'progress' in call.
Insert 'progress: <#Binding<CGFLoat>#>'

This is because we haven’t provided progress value for the Live Preview.

Change it to:

ProgressBar(progress: .constant(25.0))

You can create a constant Binding values using .constant(value) Try changing the value of progress to something else. ProgressBar will fill accordingly.

Now go back to ContentView.swift. Create a @State variable called progress of type CGFloat.

When the value of State variable changes, the view invalidates its appearance and recomputes the body. Read more on Apple Developer Documentation

@State var progress: CGFloat = 0.0

Next, add a VStack in the body of ContentView. This VStack will contain the ProgressBar we just created, and a button which will randomly set the progress when tapped.

VStack {
    ProgressBar(progress: $progress)
    Button(
        action: {
            self.progress = CGFloat.random(in: 0...100)
        }
    ) {
        Text("Random Progress")
    }
}

Now run the app and it should give you a random progress when button is tapped.

Adding animation when progress is changed

In ProgressBar.swift file, add a @State variable called isShown of type Bool, and set to false initially.

Add .onAppear { } at the end of ZStack. This will be called as soon as the ZStack is appeared. In .onAppear { } set the value of isShown to true.

struct ProgressBar: View {

    @Binding var progress: CGFloat
    @State var isShowing = false

    var body: some View {
        ZStack(alignment: .leading) {
            Rectangle()
                .foregroundColor(Color.gray)
                .opacity(0.3)
                .frame(width: 345.0, height: 8.0)
            Rectangle()
                .foregroundColor(Color.blue)
                .frame(width: 345.0 * (self.progress / 100.0), height: 8.0)
        }
        .onAppear {
            self.isShowing = true
        }
        .cornerRadius(4.0)
    }

}

Next, change the frame of progress bar to be like:

.frame(width: self.isShown ? 345.0 * (progress / 100.0) : 0, height: 8.0)

It will give the blue progress bar rectangle a width of 0 initially, but as soon as the ZStack appears, it will change its width to the progress provided.

Now adding animation is super easy.

Add this code below the frame line of blue rectangle.

.animation(.linear(duration: 0.6))

This adds a linear animation of duration 0.6 seconds to the rectangle. You can also choose ease-in or ease-out instead of linear.

Try playing with the animation duration and animation type.

Your code should now look like this:

import SwiftUI

struct ProgressBar: View {

    @State var isShowing = false
    @Binding var progress: CGFloat

    var body: some View {
        ZStack(alignment: .leading) {
            Rectangle()
                .foregroundColor(Color.gray)
                .opacity(0.3)
                .frame(width: 345.0, height: 8.0)
            Rectangle()
                .foregroundColor(Color.blue)
                .frame(width: self.isShowing ? 345.0 * (self.progress / 100.0) : 0.0, height: 8.0)
                .animation(.linear(duration: 0.6))
        }
        .onAppear {
            self.isShowing = true
        }
        .cornerRadius(4.0)
    }
}

struct ProgressBar_Previews: PreviewProvider {
    static var previews: some View {
        ProgressBar(progress: .constant(25.0))
    }
}

Finally, run the app in Live Preview of ContentView.swift, and press the button. Now the progress should animate beautifully to random value when the button is tapped.

Adding GeometryReader

Here’s a little challenge for you 😉

Try changing the progress bar frame to be dynamic rather than fixed. So that when you use change the frame of ProgressView() in ContentView.swift, its size should change according to set value rather than always being of size 345 (width) by 8 (height).

Hints:
1. You’ll need to use GeometryReader.
2. You can get width using geometry.size.width

Finished source is available on my GitHub repository.

Tagged with: