Building a Custom Progress Bar in SwiftUI

In SwiftUI, providing users with clear feedback on ongoing processes is crucial for a good user experience. A custom progress bar can be tailored to fit the design and functionality of your app perfectly. This blog post will guide you through creating a customizable progress bar in SwiftUI from scratch.

What is a Custom Progress Bar?

A progress bar is a visual element used to indicate the progression of a task to the user. Unlike the default progress indicators, a custom progress bar allows you to control every aspect of its appearance, from color and shape to animations and text overlays.

Why Build a Custom Progress Bar?

  • Brand Consistency: Tailor the look and feel to match your app’s design.
  • Advanced Features: Add custom animations, labels, and interactions.
  • Flexibility: Customize behavior and appearance based on specific app requirements.

How to Build a Custom Progress Bar in SwiftUI

Follow these steps to create a customizable progress bar in SwiftUI:

Step 1: Define the Basic Structure

Start by creating a basic structure using GeometryReader, ZStack, and Rectangle shapes:


import SwiftUI

struct CustomProgressBar: View {
    @Binding var progress: CGFloat
    var color: Color = Color.blue
    var height: CGFloat = 20

    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                Rectangle()
                    .frame(width: geometry.size.width, height: height)
                    .opacity(0.3)
                    .foregroundColor(color)

                Rectangle()
                    .frame(width: min(CGFloat(progress) * geometry.size.width, geometry.size.width), height: height)
                    .foregroundColor(color)
                    .animation(.linear, value: progress) // Smooth animation
            }
        }.frame(height: height)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct ContentView: View {
    @State private var progress: CGFloat = 0.5

    var body: some View {
        VStack {
            CustomProgressBar(progress: $progress, color: .green, height: 30)
                .padding()

            Slider(value: $progress, in: 0...1, step: 0.01)
                .padding()

            Text("Progress: ((progress * 100).formatted(.number.precision(.fractionLength(0))))%")
                .padding()
        }
    }
}

In this example:

  • @Binding var progress: CGFloat: Binds the progress value to a variable outside the view, allowing external control.
  • color: Color = Color.blue and height: CGFloat = 20: Defines customizable color and height parameters.
  • GeometryReader: Allows the progress bar to dynamically adapt to its container’s size.
  • ZStack: Layers a base rectangle with reduced opacity and a colored rectangle representing the progress.
  • animation(.linear, value: progress): Animates the progress bar’s changes smoothly.

Step 2: Add Customizable Parameters

Enhance the progress bar by adding more customizable parameters, such as corner radius and gradient colors.


import SwiftUI

struct CustomProgressBar: View {
    @Binding var progress: CGFloat
    var startColor: Color = Color.blue
    var endColor: Color = Color.blue
    var height: CGFloat = 20
    var cornerRadius: CGFloat = 10

    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                RoundedRectangle(cornerRadius: cornerRadius)
                    .frame(width: geometry.size.width, height: height)
                    .opacity(0.3)
                    .foregroundColor(startColor)

                RoundedRectangle(cornerRadius: cornerRadius)
                    .fill(
                        LinearGradient(
                            gradient: Gradient(colors: [startColor, endColor]),
                            startPoint: .leading,
                            endPoint: .trailing
                        )
                    )
                    .frame(width: min(CGFloat(progress) * geometry.size.width, geometry.size.width), height: height)
                    .animation(.linear, value: progress)
            }
        }
        .frame(height: height)
    }
}

struct ContentView: View {
    @State private var progress: CGFloat = 0.6

    var body: some View {
        VStack {
            CustomProgressBar(progress: $progress, startColor: .orange, endColor: .red, height: 30, cornerRadius: 15)
                .padding()

            Slider(value: $progress, in: 0...1, step: 0.01)
                .padding()

            Text("Progress: ((progress * 100).formatted(.number.precision(.fractionLength(0))))%")
                .padding()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Key enhancements:

  • startColor and endColor: Defines gradient colors for a visually appealing progress bar.
  • cornerRadius: Makes the progress bar rounded for a softer look.
  • LinearGradient: Applies a gradient to the progress bar.
  • Usage in ContentView to showcase the customizable options.

Step 3: Add a Label and Animation

Enhance the user experience by adding a progress label and animating the progress bar. Update the CustomProgressBar to add an animated overlay with the progress value:


import SwiftUI

struct CustomProgressBar: View {
    @Binding var progress: CGFloat
    var startColor: Color = Color.blue
    var endColor: Color = Color.blue
    var height: CGFloat = 20
    var cornerRadius: CGFloat = 10
    var showLabel: Bool = true

    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                RoundedRectangle(cornerRadius: cornerRadius)
                    .frame(width: geometry.size.width, height: height)
                    .opacity(0.3)
                    .foregroundColor(startColor)

                RoundedRectangle(cornerRadius: cornerRadius)
                    .fill(
                        LinearGradient(
                            gradient: Gradient(colors: [startColor, endColor]),
                            startPoint: .leading,
                            endPoint: .trailing
                        )
                    )
                    .frame(width: min(CGFloat(progress) * geometry.size.width, geometry.size.width), height: height)
                    .animation(.linear, value: progress)
            }
            if showLabel {
                Text("((progress * 100).formatted(.number.precision(.fractionLength(0))))%")
                    .font(.caption)
                    .foregroundColor(.white)
                    .padding(5)
                    .frame(width: geometry.size.width, alignment: .center)
                    .transition(.opacity)
            }
        }
        .frame(height: height)
    }
}

struct ContentView: View {
    @State private var progress: CGFloat = 0.7

    var body: some View {
        VStack {
            CustomProgressBar(progress: $progress, startColor: .green, endColor: .blue, height: 30, cornerRadius: 15, showLabel: true)
                .padding()

            Slider(value: $progress, in: 0...1, step: 0.01)
                .padding()

            Text("Progress: ((progress * 100).formatted(.number.precision(.fractionLength(0))))%")
                .padding()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

New additions include:

  • showLabel: Determines whether to show the progress percentage label over the bar.
  • Text overlay with animation, displaying the percentage of progress on the progress bar

Complete Code

Below is the final code integrating all of the enhancements


import SwiftUI

struct CustomProgressBar: View {
    @Binding var progress: CGFloat
    var startColor: Color = Color.blue
    var endColor: Color = Color.blue
    var height: CGFloat = 20
    var cornerRadius: CGFloat = 10
    var showLabel: Bool = true

    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                RoundedRectangle(cornerRadius: cornerRadius)
                    .frame(width: geometry.size.width, height: height)
                    .opacity(0.3)
                    .foregroundColor(startColor)

                RoundedRectangle(cornerRadius: cornerRadius)
                    .fill(
                        LinearGradient(
                            gradient: Gradient(colors: [startColor, endColor]),
                            startPoint: .leading,
                            endPoint: .trailing
                        )
                    )
                    .frame(width: min(CGFloat(progress) * geometry.size.width, geometry.size.width), height: height)
                    .animation(.linear, value: progress)
                if showLabel {
                    Text("((progress * 100).formatted(.number.precision(.fractionLength(0))))%")
                        .font(.caption)
                        .foregroundColor(.white)
                        .padding(5)
                        .frame(width: geometry.size.width, alignment: .center)
                        .transition(.opacity)
                }
            }
        }
        .frame(height: height)
    }
}

struct ContentView: View {
    @State private var progress: CGFloat = 0.7

    var body: some View {
        VStack {
            CustomProgressBar(progress: $progress, startColor: .green, endColor: .blue, height: 30, cornerRadius: 15, showLabel: true)
                .padding()

            Slider(value: $progress, in: 0...1, step: 0.01)
                .padding()

            Text("Progress: ((progress * 100).formatted(.number.precision(.fractionLength(0))))%")
                .padding()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Conclusion

Building a custom progress bar in SwiftUI offers a powerful way to tailor your app’s UI to your exact needs. With the ability to customize colors, shapes, labels, and animations, you can create a progress bar that enhances user experience and reinforces your brand identity. Experiment with different styles and parameters to find the perfect fit for your application.