Custom progress bars in SwiftUI

Share on facebook
Share on twitter
Share on pinterest

Have you ever had to implement a certain progress bar into your app, for example, to display an ongoing loading process? Well, in SwiftUI it’s super simple to create these 👇

I also uploaded the progress bars templates to GitHub. So if you want to skip this tutorial, you can just copy and paste the files to your project and initialize the specific bars with multiple customization options.

Simple Progress Bar

Let’s start with creating a simple progress bar like this one:


First, create a new SwiftUI file called SimpleProgressBar.

Create a State property assigned to a CGFloat value. We use this to represent the current progress of our loading process. For example, 0.5 means that 50 percent have loaded so far.

struct SimpleProgressBarDemo: View {
    @State var currentProgress: CGFloat = 0.0

    var body: some View {
        Text("Hello World")
    }
}

The next step to create our progress bar is to replace the default Text view with a ZStack with the leading alignment mode.

var body: some View {
    ZStack(alignment: .leading) {

        }
    }

ZStacks are used for stacking elements on top of each other. Click here to learn more. The first object in our ZStack is the „inner“, static bar. To create this, we use a RoundedRectangle.

ZStack(alignment: .leading) {
            RoundedRectangle(cornerRadius: 20)
        }

We want the bar to be gray, wide and thin.

RoundedRectangle(cornerRadius: 20)
                .foregroundColor(.gray)
                .frame(width: 300, height: 20)

Another, dynamic bar should overlay our static one. Therefore, we place another RoundedRectangle into our ZStack. This rectangle should be as high as the first one but only as wide as it if our currentProgress is 1.0, i.e. 100 percent are loaded. To achieve this, we write:

RoundedRectangle(cornerRadius: 20)
                .foregroundColor(.blue)
                .frame(width: 300*currentProgress, height: 20)

To simulate a loading process, we use a timer that adds 0.1 to our currentProgress every second. We implement a corresponding button that starts the timer.

struct SimpleProgressBarDemo: View {
    
    @State var currentProgress: CGFloat = 0.0
    
    var body: some View {
        VStack {
            ZStack(alignment: .leading) {
                //...
            }
            Button(action: {self.startLoading()}) {
                Text("Start timer")
            }
        }
    }
    
    func startLoading() {
        _ = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
            withAnimation() {
                self.currentProgress += 0.01
                if self.currentProgress >= 1.0 {
                    timer.invalidate()
                }
            }
        }
    }
}

By using the withAnimation wrapper, we apply a smooth, default animation to our bar’s progress. Run the view in the live preview and click the button to simulate the loading process.

Circular Progress Bar


First, create a new SwiftUI file called CircularProgressBar. Let’s implement a State for keeping track of the loading progress.

struct CircularProgressBarDemo: View {
    
    @State var circleProgress: CGFloat = 0.0
    
    var body: some View {
        Text("Hello World!")
    }
}

Replace the default Text view with a ZStack again.

ZStack {
            
        }

The first object in our ZStack is the „inner“, static circle. To create it, declare a Circle object.

ZStack {
            Circle()
        }

We don’t want our circle filled out, we just want it to be a ring. To do this we use the .stroke modifier. Next, we modify the width and height of our circle by using the .frame modifier.

Circle()
    .stroke(Color.gray, lineWidth: 15)
    .frame(width: 200, height: 200)

Another, dynamic circle should overlay the static one. Therefore, place a new Circle view into the ZStack. We use the .stroke and .frame modifier again, but this time we use a different color.

Circle()
     .stroke(Color.blue, lineWidth: 15)
     .frame(width: 200, height: 200)

The circle should only be closed when our circleProgress State is 1.0. For example, if the progress state is 0.5, the ring should only be half long. To achieve this we use the .trim modifier (as the first modifier!) and pass the circleProgress to its end argument.

Circle()
    .trim(from: 0.0, to: circleProgress)
    .stroke(Color.blue, lineWidth: 15)
    .frame(width: 200, height: 200)

Now try to change the value of the circleProgress State. You see that our circular progress bar immediately adapts to it.But we don’t want our circular progress bar to start at the top. Therefore, we spin it by 90 degrees.

Circle()
     .trim(from: 0.0, to: circleProgress)
     .stroke(Color.blue, lineWidth: 15)
     .frame(width: 200, height: 200)
     .rotationEffect(Angle(degrees: -90))

If you want, you can also insert a Text indicating how much percent is already loaded.

ZStack {
            //...
            Text("\(Int(self.circleProgress*100))%")
                .font(.custom("HelveticaNeue", size: 20.0))
        }

To simulate the loading process, we implement a corresponding button that fires a timer again.

struct CircularProgressBarDemo: View {
    
    @State var circleProgress: CGFloat = 0.0
    
    var body: some View {
        VStack {
            ZStack {
                //...
            }
            Button(action: {self.startLoading()}) {
                Text("Start timer")
            }
        }
    }
    
    func startLoading() {
        _ = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
            withAnimation() {
                self.circleProgress += 0.01
                if self.circleProgress >= 1.0 {
                    timer.invalidate()
                }
            }
        }
    }
}

Tip: Subtract twice the line width from the width and height of the circle to achieve a style like this:

ZStack {
                Circle()
                    //...
                Circle()
                    .trim(from: 0.0, to: circleProgress)
                    .stroke(Color.blue, lineWidth: 15)
                    .frame(width: 200-15*2, height: 200-15*2)
                    .rotationEffect(Angle(degrees: -90))
                //...
            }

Halved-Circular Progress Bar

Creating this cool bar is very similar to what we’ve already done.


As always, we create a State property to know how far the loading process is already. We also replace the default text with a ZStack.

    @State var progress: CGFloat = 0.0
    
    var body: some View {
        ZStack {
            
        }
    }

Again, we start by creating a static circle. Only this time we trim it to the half. We also spin it by 180 degrees. To create a dashed ring we use the .stroke modifier again, but this time we use a different StrokeStyle.

Circle()
                .trim(from: 0.0, to: 0.5)
                .stroke(Color.blue, style: StrokeStyle(lineWidth: 12.0, dash: [8]))
                .frame(width: 200, height: 200)
                .rotationEffect(Angle(degrees: -180))

We equip our dynamic circle with the usual .stroke modifier and trim it depending on our loading process. We also spin it by 180 degrees.

ZStack {
            Circle()
                //...
            Circle()
                .trim(from: 0.0, to: progress/2)
                .stroke(Color.blue, lineWidth: 12.0)
                .frame(width: 200, height: 200)
                .rotationEffect(Angle(degrees: -180))
        }

You can add an according Text again and simulate the loading process with a timer.

struct HalvedCircularBar: View {
    
    @State var progress: CGFloat = 0.0
    
    var body: some View {
        VStack {
            ZStack {
                Circle()
                    .trim(from: 0.0, to: 0.5)
                    .stroke(Color.blue, style: StrokeStyle(lineWidth: 12.0, dash: [8]))
                    .frame(width: 200, height: 200)
                    .rotationEffect(Angle(degrees: -180))
                Circle()
                    .trim(from: 0.0, to: progress/2)
                    .stroke(Color.blue, lineWidth: 12.0)
                    .frame(width: 200, height: 200)
                    .rotationEffect(Angle(degrees: -180))
                Text("\(Int(self.progress*100))%")
                    .font(.custom("HelveticaNeue", size: 20.0))
            }
            Button(action: {self.startLoading()}) {
                Text("Start timer")
            }
        }
    }
    
    func startLoading() {
        _ = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
            withAnimation() {
                self.progress += 0.01
                if self.progress >= 1.0 {
                    timer.invalidate()
                }
            }
        }
    }
}

Activity Indicator

Although this is not really a progress bar, we want to have a look at how to create an activity indicator in SwiftUI.


We declare a State again, but this time for keeping track of the current indicator’s position by its degree.

@State var degress = 0.0

As usual, we use a static and a dynamic circle. We trim the dynamic circle by a fixed value. To rotate the whole dynamic circle, we use the .rotationEffect modifier and make it dependent on the value of our degrees State.

Circle()
                .trim(from: 0.0, to: 0.6)
                .stroke(darkBlue, lineWidth: 5.0)
                .frame(width: 120, height: 120)
                .rotationEffect(Angle(degrees: degress))

To present a loading process, we can simply use a repeating timer that starts as soon as the view has loaded. The timer adds a few degrees to our State at fast time intervals. Once our circle has one full time, we set our degree State back to 0.

struct ActivityIndicator: View {
    
    @State var degress = 0.0
    
    var body: some View {
        Circle()
            .trim(from: 0.0, to: 0.6)
            .stroke(darkBlue, lineWidth: 5.0)
            .frame(width: 120, height: 120)
            .rotationEffect(Angle(degrees: degress))
            .onAppear(perform: {self.start()})
    }
    
    func start() {
        _ = Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true) { timer in
            withAnimation {
                self.degress += 10.0
            }
            if self.degress == 360.0 {
                self.degress = 0.0
            }
        }
    }
}

Conclusion 🎊

Great, we just learned how easy it is to create beautiful progress bars in SwiftUI. If you want to see more, make sure you follow us on Instagram and subscribe to our newsletter to not miss any updates, tutorials and tips about SwiftUI and more!

Leave a Reply

Your email address will not be published. Required fields are marked *

small_c_popup.png

Covid-19 Forces you into quarantine?

Start Mastering swiftUI Today save your 33% discount

small_c_popup.png

Are you ready for a new era of iOS development?

Start learning swiftUI today - download our free e-book