Creating a ProgressView Modal in SwiftUI

Back in June at WWDC20, Apple announced the latest updates for SwiftUI. One of the updates that stood out for me was the addition of ProgressView. ProgressView is a very powerful view that will allow developers to show the progress of a particular task, either by displaying a progress wheel or in a horizontal bar. Additionally you can show progress indeterminately in the form of an Activity Indicator.

In the first iteration of SwiftUI, to use an Activity Indicator you had to embed the UIKit UIActivityIndicatorView inside a SwiftUI UIViewRepresentable, and make use of that. With ProgressView, that is now a thing of the past.

Displaying a ProgressView in a modal#

It’s all well and good displaying a ProgressView, however you may want to prevent users from interacting with your app whilst you are performing a task, such as fetching data from a server, for example. To do this, you can display the ProgressView inside a modal view that appears over your content.

First we need to create the modal view:

struct LoadingView<Content>: View where Content: View {

    @Binding var isShowing: Bool  // should the modal be visible?
    var content: () -> Content  
    var text: String?  // the text to display under the ProgressView - defaults to "Loading..."

    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .center) {
                // the content to display - if the modal is showing, we'll blur it
                content()
                    .disabled(isShowing)
                    .blur(radius: isShowing ? 2 : 0)
                
                // all contents inside here will only be shown when isShowing is true
                if isShowing {
                    // this Rectangle is a semi-transparent black overlay
                    Rectangle()
                        .fill(Color.black).opacity(isShowing ? 0.6 : 0)
                        .edgesIgnoringSafeArea(.all)

                    // the magic bit - our ProgressView just displays an activity
                    // indicator, with some text underneath showing what we are doing
                    VStack(spacing: 48) {
                        ProgressView().scaleEffect(2.0, anchor: .center)
                        Text(text ?? "Loading...").font(.title).fontWeight(.semibold)
                    }
                    .frame(width: 250, height: 200)
                    .background(Color.white)
                    .foregroundColor(Color.primary)
                    .cornerRadius(16)
                }
            }
        }
    }
}

Once we have created the modal, using it is as simple as encapsulating the contents of your view inside of it.

struct ContentView: View {
    
    @State var loadingViewShowing = false
    
    var body: some View {
        // Your entire view should go inside the LoadingView, so that the modal
        // can appear on top, as well as blur the content
        LoadingView(isShowing: $loadingViewShowing) {
            Button(action: {
                loadingViewShowing = true
                // Mock some network request or other task
                DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                    loadingViewShowing = false
                }
            }, label: {
                Text("Tap Me!")
            })
        }
    }
}

Run the app, and tap the button. You’ll see the modal appear, and then disappear after 3 seconds. You can display it whilst a network request is in progress by simply setting loadingViewShowing to true before the request is sent, and then setting it to false once the request is complete.

You could even extend this to show a determinate ProgressView, which would show your users how much progress has been made towards completing the task.

Conclusion#

That concludes this tutorial. If you have any questions, feel free to reach out to me via Twitter, and keep an eye open for my upcoming tutorials.

The completed solution can be found here on GitHub.