How to show progress on a task using ProgressView in SwiftUI

swiftui Jul 25, 2024
How to show progress on a task using ProgressView in SwiftUI

In this tutorial, you will learn how to make use of ProgressView in SwiftUI to show progress while the data of the View is fetched during an API call.

To make the tutorial more interesting, we'll be using an open jokes API. When the app is launched, the jokes will be fetched using the API and user will be able to refresh the jokes feed with the help of a refresh button.

This is how it is going to look like:

The idea here is to conditionally render the ProgressView on the View when the API call is made and hide it as soon as we receive the response from the server. To achieve this, we will make use of a boolean @State variable isFetching.

func fetchJokes() {
    isFetching = true
    let url = URL(string: "https://official-joke-api.appspot.com/jokes/random/5")!
        
    Task {
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            let decodedJokes = try JSONDecoder().decode([Joke].self, from: data)
            DispatchQueue.main.async {
                self.jokes = decodedJokes
                self.isFetching = false
            }
        } catch {
            print("Failed to fetch jokes: \(error)")
            DispatchQueue.main.async {
                self.isFetching = false
            }
        }
    }
}

When the above function gets called, isFetching variable will set to true and as a result, ProgressView will be displayed on the view. Once the network request completes, the response will be either a success or failure. If the response is success, then the jokes array is updated with the new jokes and isFetching is again set to false. Otherwise, we'll print the error and set isFetching to false.

Now, let's build the UI for our app.

struct Joke: Identifiable, Hashable, Decodable {
    var id: Int
    var setup: String
    var punchline: String
}

struct ContentView: View {
    @State private var jokes: [Joke]?
    @State private var isFetching: Bool = false
    
    var body: some View {
        VStack {
            if isFetching {
                ProgressView("Loading jokes for you")
                    .controlSize(.large)
            } else {
                if let jokes = jokes {
                    List(jokes) { joke in
                        VStack(spacing: 10) {
                            Text(joke.setup)
                            Text(joke.punchline)
                        }
                        .padding()
                    }
                    .listStyle(PlainListStyle())
                } else {
                    Text("No jokes available.")
                }
                Spacer()
            }
        }
        .onAppear {
            fetchJokes()
        }
    }
}

Result:

In the above code:

  1. We first create a model for the joke that we want to display. It contains a subset of variables to be returned by the JSON object.
  2. We have created two state variables, one for the jokes array as it is going to get updated everytime the API is hit and the other one isFetching initialised to false.
  3. To give more context about the ongoing task, a description is added to the ProgressView passed as a String. Also, size of the ProgressView has been increased using the .controlSize modifier.
  4. If the jokes are not fetched successfully, an alternate text "No jokes available" is displayed.
  5. Lastly, call fetchJokes() in the onAppear modifier because as soon as the view appears this method should be called.

For the sake of simplicity, we are not following any architecture design like MVVM. If we were using the MVVM pattern then all the logic would go to ViewModel class.

Now, let's also create a Refresh button within the VStack which calls the refreshJokes() function.

Button(action: refreshJokes) {
    Text("Refresh")
        .padding()
        .background(Color.blue)
        .foregroundColor(.white)
        .cornerRadius(10)
}
.padding()

The last step is to create the refreshJokes() function which fetches new set of jokes everytime the user clicks on the Refresh button. For that, we will simply set the jokes array to nil and call the fetchJokes()function again. Since, our API doesn't support pagination.

func refreshJokes() {
    jokes = nil
    fetchJokes()
}

Result:

If you want to style it better, you can follow the full source code available here.

 

Where to go next?

Today you learned you learned how to make make use of ProgressView to indicate the user that a task is being performed in the background and the app is not stuck. As an iOS developer, ProgressView is one of those components that used quite frequently in the application therefore, you should definitely have a good command on this concept.

To expand your knowledge about ProgressView, we would highly recommend you to explore articles like Progress View in SwiftUI and How to create a Circular Progress Bar in SwiftUI.

Signup now to get notified about our
FREE iOS Workshops!