Circular Progress bar view in SwiftUI

DevTechie Inc
Jun 11, 2022

Photo by Jeremy Perkins on Unsplash

SwiftUI has built-in progress bar which comes in two flavors, circular and linear but if you are looking for a gradient progress bar then you will have to build one yourself.

Given its SwiftUI, its needless to say that even creating custom progress bar is much simpler and takes minutes to build up from scratch.

Today, we will be building a circular progress control from ground up. Here is what we will have by the end of this article:

Let’s get started. We will add a new SwiftUI view and call it “CustomProgressView.swift”

We will need a binding variable to show progress on this view so we will add variable called “progress”. Our view will look something like this:

import SwiftUIstruct CustomProgressView: View {
    @Binding var progress: CGFloat
    
    }
}
Notice @Binding variable, this will allow our view to receive progress value from the parent view and our progress will be displayed on the screen.

Next we will add two circles in ZStack. Because we are using ZStack and content stacks on top of each other in z-index, first circle will work as track on which our progress bar will be filled. We will also set width and height of our control to be 200x200. Code will look something like this:

import SwiftUIstruct CustomProgressView: View {
    @Binding var progress: CGFloat
    var body: some View {
        ZStack {
            // progress track
            Circle()
            
            // progress circle
            Circle()
            
        }
        .frame(width: 200, height: 200)
        .padding()
    }
}
Now is the time to style our circles with some modifiers. Since we are creating circular progress bar, we will draw stroke for these circles with the help of stroke modifier.

For progress track circle, we will add foreground color as gray color and set opacity to be 0.2.

For progress circle, we will add trim modifier which trims the shape by a fractional amount based on its representation as a path. Trim expects from and to values, so for from, we will start our progress with 0.0 and to will become min of progress or 1.0.

We will also set stroke with AngularGradient, and set stroke style with lineWidth, lineCap and lineJoin.

import SwiftUIstruct CustomProgressView: View {
    @Binding var progress: CGFloat
    var body: some View {
        ZStack {
            // placeholder
            Circle()
                .stroke(lineWidth: 20)
                .foregroundColor(.gray)
                .opacity(0.2)
            
            // progress circle
            Circle()
                .trim(from: 0.0, to: min(progress, 1.0))
                .stroke(AngularGradient(colors: [.yellow, .orange, .pink, .red], center: .center), style: StrokeStyle(lineWidth: 20, lineCap: .butt, lineJoin: .miter))
            
        }
        .frame(width: 200, height: 200)
        .padding()
    }
}
This will create our progress bar but angle is not correct as we want our progress to start from top and end at the top so we will use rotationEffect modifier with -90 degrees angle. We will also add shadow and add a Text view at the center to display progress in percentage. Our completed control will look like this:

struct CustomProgressView: View {
    @Binding var progress: CGFloat
    var body: some View {
        ZStack {
            // placeholder
            Circle()
                .stroke(lineWidth: 20)
                .foregroundColor(.gray)
                .opacity(0.2)
            
            // progress circle
            Circle()
                .trim(from: 0.0, to: min(progress, 1.0))
                .stroke(AngularGradient(colors: [.yellow, .orange, .pink, .red], center: .center), style: StrokeStyle(lineWidth: 20, lineCap: .butt, lineJoin: .miter))
                .rotationEffect(.degrees(-90))
                .shadow(radius: 2)
            
            Text("\(String(format: "%0.0f", progress * 100))%")
                .font(.largeTitle)
            
        }
        .frame(width: 200, height: 200)
        .padding()
        .animation(.easeInOut, value: progress)
    }
}
Once our progress bar is created, we will create view that will use this progress view.

We will create a slider to simulate progress and user slides the slider, our progress bar will update the progress.

struct CustomProgressViewUser: View {
    @State private var progress: CGFloat = 0.0
    var body: some View {
        VStack {
            CustomProgressView(progress: $progress)
                .padding(.bottom, 40)
            Slider(value: $progress, in: 0.0...1.0)
            
        }
        .padding()
    }
}
Our complete code will look like this:

import SwiftUIstruct CustomProgressView: View {
    @Binding var progress: CGFloat
    var body: some View {
        ZStack {
            // placeholder
            Circle()
                .stroke(lineWidth: 20)
                .foregroundColor(.gray)
                .opacity(0.2)
            
            // progress circle
            Circle()
                .trim(from: 0.0, to: min(progress, 1.0))
                .stroke(AngularGradient(colors: [.yellow, .orange, .pink, .red], center: .center), style: StrokeStyle(lineWidth: 20, lineCap: .butt, lineJoin: .miter))
                .rotationEffect(.degrees(-90))
                .shadow(radius: 2)
            
            Text("\(String(format: "%0.0f", progress * 100))%")
                .font(.largeTitle)
            
        }
        .frame(width: 200, height: 200)
        .padding()
        .animation(.easeInOut, value: progress)
    }
}struct CustomProgressViewUser: View {
    @State private var progress: CGFloat = 0.0
    var body: some View {
        VStack {
            CustomProgressView(progress: $progress)
                .padding(.bottom, 40)
            Slider(value: $progress, in: 0.0...1.0)
            
        }
        .padding()
    }
}struct CustomProgressView_Previews: PreviewProvider {
    static var previews: some View {
        CustomProgressViewUser()
    }
}
Final output will look like this:

With that we have reached the end of this article. Thank you once again for reading. Don’t forget to subscribe to our weekly newsletter at