SwiftUI Progress View - Showing Time Progress

The progress view, also known as activity indicator or spinner in UIKit, is a SwiftUI view that indicates visually the progress of a task towards its completion. There is an older post where I present the progress view, and if you just want to get the grasp of it, please take a few minutes to give it a read.

In this post, the goal is to meet a new capability of the progress view; time progress display, based on a given time interval that we provide it with. This kind of progress reporting is suitable when counting time and we want to also show a visual representation of that, such as in a count down.

Note

This new progress view capability was announced in WWDC 2022, and it's available as of iOS 16 and macOS 13.

Implementing a progress view with time interval

In its simplest form, a progress view that displays time based progress is initialized with one required argument only; the time interval that will be animating the progress for.

There is an important detail to note here; time interval is not a TimeInterval (aka Double) value, but a closed Date range like this:

startDate...endDate

Given that, the most handy approach in my opinion is to prepare this closed range before use, and then pass it as argument to the progress view at the initialization point.

With that in mind, let's declare a computed property in the SwiftUI view that returns a closed Date range similar to the previous example. In its body we'll specify two dates; the start and end dates, with the second representing a timestamp five (5) seconds after the first one. As a last step, we will form a close range using these two dates and we'll return them from the property.

Here is what I just described in code:

struct ContentView: View {
    var progressInterval: ClosedRange<Date> {
        let start = Date()
        let end = start.addingTimeInterval(5)
        return start...end
    }
    
    // View's body implementation...
    ...
}

Two things to note here:

  1. There are more ways to initialize the start and end date instances, but the above is the fastest one for this particular example.
  2. It's not necessary to create a computed property when it becomes necessary to use a time based progress view. We could use a function instead, accepting values that would form the date range dynamically. Or even faster, to build the closed range inline right when the progress view is initialized. However, for clarity and the sake of the demonstration, the above is what we'll stick with here.

With the progressInterval in place, let's initialize a progress view in the simplest possible way:

var body: some View {
    ProgressView(timerInterval: progressInterval)
        .padding()
}

See that the progressInterval is supplied as argument to the ProgressView(timeInterval:) initializer. This is enough if we don't want to apply any styling with view modifiers; the padding shown above is to prevent the progress view from sticking to the screen edges:

You can notice from the above demonstration that the progress is going towards the empty state instead of getting filled, however that's the default behavior. To change it and make the progress keep filling the view, it's necessary to provide an additional, optional argument to the initializer, setting the false value to it:

ProgressView(timerInterval: progressInterval, countsDown: false)

A shown in the above two visual samples, progress view has a built-in label reporting the time, with the displayed value also depending on the countsDown parameter value.

There is one more argument that we can optionally provide to the progress view; the content of a label that's displayed right above the progress view:

ProgressView(timerInterval: progressInterval, countsDown: true, label: {
    Text("The count down has started!")
})

The above is all it takes in order to show a progress view that will report progress for a specific date range. One last thing to keep in mind is that everything we met work for the linear progress view style; applying a circular style has no effect and the spinner will animate forever.

Time based progress view with custom styles

This particular variation of the progress view can get a custom style, which we implement in a separate struct conforming to ProgressViewStyle. As example, we'll use the custom style implementation taken from the previous progress view tutorial:

struct WithBackgroundProgressViewStyle: ProgressViewStyle {
    func makeBody(configuration: Configuration) -> some View {
        ProgressView(configuration)
            .padding(8)
            .background(Color.gray.opacity(0.25))
            .tint(.red)
            .cornerRadius(8)
    }
}

Now let's apply it to the progress view:

ProgressView(timerInterval: progressInterval, countsDown: true, label: {
    Text("The count down has started!")
})
.progressViewStyle(WithBackgroundProgressViewStyle())

Here's the result :

In the above custom style there is a ProgressView instance that gets initialized in the makeBody(configuration:) method. The following, however, is not going to work; also taken from the previous tutorial, that custom style does not create a new ProgressView instance Instead, it represents progress by combining other views and using data from the configuration parameter value:

struct RoundedRectProgressViewStyle: ProgressViewStyle {
    func makeBody(configuration: Configuration) -> some View {
        ZStack(alignment: .leading) {
            RoundedRectangle(cornerRadius: 14)
                .frame(width: 250, height: 28)
                .foregroundColor(.blue)
                .overlay(Color.black.opacity(0.5)).cornerRadius(14)
            
            RoundedRectangle(cornerRadius: 14)
                .frame(width: CGFloat(configuration.fractionCompleted ?? 0) * 250, height: 28)
                .foregroundColor(.yellow)
        }
        .padding()
    }
}

As it seems, the fractionCompleted property of the Configuration type does not get updated when using a date range with the progress view, so no progress is indicated here. That's a useful piece of information to know in advance, so you avoid implementing custom styles that most probably won't lead to the desired result.

Conclusion

Time based progress view is a useful addition to SwiftUI. The only downside I find is the inability to create fully custom styles that would fit to the appearance of custom interfaces. Regardless, even with none or a slight customization, this new kind of progress view remains quite interesting. Thanks for reading, take care!