Home iOS & Swift Tutorials

Spinner and Progress Bar in Swift: Getting Started

Learn how to implement a spinner indicator and progress bar in both UIKit and SwiftUI.

4.5/5 4 Ratings


  • Swift 5.5, iOS 15, Xcode 13

As iOS developers, you’re always thinking of how to make your users’ experience better. One surefire way to improve their experience is to use progress indicators while content loads. Progress indicators alleviate frustration by letting users know the app is working, not ignoring their requests.

Swift toolbox

In this tutorial, you’ll build two kinds of progress indicators in an iOS app that displays high-quality photos of dahlias, a type of flower. Specifically, you’ll learn how to:

  • Use UIActivityIndicatorView to show indeterminate progress.
  • Integrate UIProgressView to show determinate progress.
  • Use SwiftUI’s ProgressView.

This tutorial assumes you know the basics of building iOS apps in either UIKit or SwiftUI and have some familiarity with making network requests using URLSession. Check out our Networking with URLSession course if you want to know more about fetching data from the internet.

OK. Time to start!

Getting Started

Download the starter project using the Download Materials button at the top or bottom of this tutorial.

Build and run.

The sample project on launch

The sample app has four UIViewControllers within a UITabController.

Open FirstViewController.swift.

This small view controller is currently making a network request to the CDN server hosted on a website for a picture of a dahlia. You make the network request then load the image inside UIImageView, in Main.storyboard.

That network request won’t happen instantly, so you’ll add a progress indicator to let the user know what’s happening while they wait. But which kind of indicator should you use? Next, you’ll see when to use a spinner versus a progress bar.

Different Types of Progress Indicators

In this tutorial, you’ll build two different progress indicators:

  • Spinner: An indeterminate progress indicator.
  • Progress bar: A determinate progress indicator.

To understand the difference between the two, imagine this scenario:

It’s a hot day. You want ice cream, but there are six people in front of you. You don’t know how long it’ll take to get to the front of the line, but you know you’ll get your ice cream eventually. Your wait time is indeterminate.

The next hot day, you get in line for ice cream again. There are eight people in front of you now, but this time, there’s a sign telling you how long it’ll take to get served. Great news, it’ll only take five minutes! That’s a determinate wait time.

In your app’s case, you don’t know how long the image loading process will take, so you’ll implement an indeterminate progress indicator.

Understanding UIActivityIndicatorView

Note: If you’re only interested in creating spinners and progress bars in SwiftUI, feel free to skip ahead to Creating a ProgressView With CircularProgressViewStyle in SwiftUI.

Use UIActivityIndicatorView when you don’t know how long a particular task will take. Some situations include:

  • Network calls
  • Downloading an image
  • Background processing, such as database cleanup

Before you can use a UIActivityIndicatorView, you need to create a container view to allow the progress indicator to appear on different backgrounds.

Creating a Container View for UIActivityIndicatorView

You’ll start by adding a new view to contain the UIActivityIndicator. This lets you hide the whole view when the image finishes downloading, but it also means you’ll need to add some styling.

Open Main.storyboard.

Find the Pink Dahlia Scene, and add a new UIView to this ViewController‘s view, just below the image view in the view hierarchy.

Create a new UIView for the FirstViewController screen

Open the Add New Constraints panel and set the UIView to 100 wide and 100 high.

Set the required constraints here

Open the Align panel and align UIView horizontally and vertically.

Set the alignment of the view

Next, you’ll hook this UIView to its UIViewController. Open a new Editor tab in Xcode.

In FirstViewController.swift, add the following code directly below the UIImageView IBOutlet:

@IBOutlet weak var loadingView: UIView! {
  didSet {
    loadingView.layer.cornerRadius = 6

You’ll see a small dot on the side. Drag this to the UIView you just added in the storyboard to make the connection. Notice that you’ve also set the cornerRadius value in the loadingView setter to 6. This makes the view look pretty. :]

Adding UIActivityIndicatorView

Now that you’ve created a container view, it’s time to add the UIActivityIndicator. This is a UIKit component that Apple provides. It’s designed to show an indeterminate loading time within your iOS app.

Open Main.storyboard.

Drag a new Activity Indicator View inside the Loading View you created in the previous step.

Add a new UIActivityIndicator

With the activity indicator selected, open the Align panel and align the view vertically and horizontally to make sure the activity indicator is centered in its container.

Next, open FirstViewController.swift.

Directly above the UIImageView IBOutlet, add the following code:

@IBOutlet weak var activityIndicator: UIActivityIndicatorView!

As you did before, open Main.storyboard in a second editor and drag the IBOutlet connection to the newly created activity indicator view.

Build and run.

The app with your basic loading view showing

Your app now displays an indeterminate loading view. But you might notice two things:

  • The loading indicator doesn’t spin.
  • The loading view still displays, even after the image downloads.

You’ll fix these two issues next.

Making UIActivityIndicatorView Spin

UIActivityIndicatorView has two methods to control the spinning:

  • startAnimating()
  • stopAnimating()

Open FirstViewController.swift.

You’ll create some private functions to start the spinner and hide the loading view after the image finishes downloading.

Directly below viewDidLoad(), add the following code:

private func showSpinner() {

private func hideSpinner() {

It’s good practice to have your own internal API design. This makes it easier to call a simple function that might do additional behavior, like hide the activity indicator’s container view.

Inside showSpinner(), add the following code:

loadingView.isHidden = false

This tells UIActivityIndicator to start spinning and makes loadingView visible.

Now, add the following code inside hideSpinner():

loadingView.isHidden = true

The first line you added stops the UIActivityIndicator from spinning. The second line lets loadingView disappear.

Finally, you need to call the internal functions. Inside viewDidLoad(), above let url = ..., add the following code:


As soon as the view loads, you’ll show the spinner.

When the image finishes downloading, you need to call hideSpinner().

Inside loadImage(with:), and directly below self.imageView.image = ..., add the following code:


As soon as a new image gets set, you’ll hide the spinner. Don’t forget to always show and hide the spinner on the main queue.

Build and run the app.

Your indeterminate indicator showing in the app

Congratulations, your indeterminate activity indicator is fully working! It now shows your users that the app is doing something while they wait for the photo to load.

Next, you’ll learn how to show a progress indicator for tasks with a determinate completion time.

Understanding UIProgressView

When you know how long a particular task will take, you use UIProgressView instead of UIActivityIndicatorView. Some examples of when to use UIProgressView include:

  • Loading an image from a cache or bundle
  • Loading dependencies within the app

To try this out, you’ll display a UIProgressView while loading the second TabBar item: a picture of a Red Delight dahlia.

But first, you need to create a container view, just like you did for UIActivityIndicator, to make the progress bar easily visible to the user.

Creating a Container View for UIProgressView

Creating a container view to hold your progress view lets you choose your progress view’s background color to make it more visible.

To create a container view, open SecondViewController.swift.

Currently, this view controller is making a network request to the raywenderlich.com image directory and displaying the fetched image in an UIImageView. You’ll expand it by adding a progress bar.

Open Main.storyboard, find the Red Delight Scene and select the LoadingView in its view hierarchy. Open another editor for the matching SecondViewController.swift. Directly below imageView, add the following code:

@IBOutlet weak var loadingView: UIView! {
  didSet {
    loadingView.layer.cornerRadius = 6

Drag the outlet to the Main.storyboard loading view. To style loadingView, make the background color a matching gray. Select the LoadingView in the storyboard, and open the Attributes inspector.

Open the Attributes inspector on your loading view

For the background Background, select System Gray 4 Color.

System Gray 4 selected from the list of background options

With this container view, you’ve ensured that the indicator will be easy to see. Not only is that best practice, but it’s also a way for you to play with the styling of your app. :]

Next, you’ll add the progress bar.

Adding UIProgressView

To create your progress bar, you need to add a UIProgressView. Still in the storyboard, drag a new Progress View from the Library inside the Loading View.

Open the Align panel and align the Progress View vertically and horizontally.

Which alignment constraints to select

Open the Add New Constraints panel and set the Width to 150 and the Height to 4 points.

In this tutorial, you’re utilizing the power of Interface Builder to remove lots of boilerplate code within your class. Of course, you can create these views programmatically, but this approach lets you see your work while you’re doing it.

Open a new editor and open SecondViewController.swift alongside Main.storyboard.

Directly below the loadingView outlet, add the following code:

@IBOutlet weak var progressView: UIProgressView!

Drag the outlet connection to your ProgressView within Main.storyboard.

Build and run. Tap the Red Delight tab bar item.

Your determinate progress view showing

Now, your Loading View has a progress view inside. But have you noticed the progress view doesn’t change? That’s because you haven’t linked the progress view to any updates or changes from the network request yet. You’ll do that next.

Linking UIProgressView to the Download Process

Open SecondViewController.swift.

You’ll take advantage of the progress updates that URLSession provides.

Directly below the progressView IBOutlet, add the following code:

private var observation: NSKeyValueObservation?

Here, you’ve created a new property that is an NSKeyValueObservation object. This object can observe key-value changes and acts like NSNotifications. When Apple fires a change, you can observe it. In this case, you’ll observe the progress updates from the URLSession network call.

Directly below the new property you added above, add the following code:

deinit {

This invalidates the observation when the class is uninitialized, preventing memory leaks and holding onto this object in the app’s lifecycle.

You have one more piece of the puzzle to finish: monitoring the changes of the network request.

Directly above task.resume(), add the following code:

observation = task.progress.observe(\.fractionCompleted) { progress, _ in
  DispatchQueue.main.async {
    self.progressView.progress = Float(progress.completedUnitCount)

This does two things:

  • Creates a new observation object on task.progress, which is the object that Apple exposes in the URLSession request.
  • Adds a callback to be called whenever the progress changes which sets the progress view’s progress to the same value as the download’s progress.

Now, build and run the project.

Your determinate progress view hasn't changed

You’ll now see that your progress view updates based on the network request changes and the image download.

But have you spotted the problem? You’re still displaying loadingView after the download finishes.

Your loading view is still showing

To close loadingView, go to the network request and directly below self.imageView.image = UIImage(data: data), add the following code.

self.loadingView.isHidden = true

This will hide the loading view as soon as an image is set.

Build and run the project and head to the Red Delight tab.

Your app is now showing a basic determinate progress view

Congratulations, you’ve just added your first determinate progress view using UIKit!

The Advantages of Using UIProgressView and UIActivityIndicatorView

The advantage of using progress indicators like a spinner or progress bar is that your app is giving the users what they want: an update about what’s going on!

Since Apple has done most of the logic work for you, adding a progress view or activity indicator view is a small effort on your part that pays enormous dividends for your user’s experience.

This is especially true with Apple’s new SwiftUI framework. You’ll explore adding ProgressView using SwiftUI next and see the differences between UIKit and SwiftUI.

SwiftUI is a super-hero new framework when building your UI

Creating a ProgressView With CircularProgressViewStyle in SwiftUI

SwiftUI uses a declarative syntax to build your UI. Unlike the work you did above in UIKit, SwiftUI’s ProgressView covers both determinate and indeterminate loading.

First, you need to create a whole new SwiftUI view.

Open the SwiftUI group, then the Views group within it. Right-click the group and select New File…. Click SwiftUI View, then click Next.

Create a brand new SwiftUI view

Name this file RWProgressView and click Create.

You’ve created a brand new SwiftUI view. Replace the pre-made contents of the body with the following code:


SwiftUI includes a ProgressView that you can use and style to your liking.

The final step is to add some styling, just like you did earlier. Add the following code directly below the progress view:

.progressViewStyle(CircularProgressViewStyle(tint: .black))

With this, you’ve created a circular indicator that’s black. You also made the background color the same as before to keep the color scheme consistent across the app. Check the preview of this SwiftUI view to see that you’ve applied the styles correctly.

Build and run.

Loading View is not yet showing in third tab

Tap the third TabBar item named Pines. You’ve created the progress view in your SwiftUI view, but it’s not currently displaying in your app. You’ll fix this by adding your SwiftUI view to the view controller.

Using UIHostingController to Host SwiftUI ProgressView in UIKit

So far, you’ve built this app using UIKit. To use SwiftUI in your UIKit app, you need to use UIHostingController.

Open ThirdViewController.swift.

Directly below the loadingView IBOutlet, add the following code:

private let progressView = RWProgressView()

This instantiates the SwiftUI view you just created.

Directly below viewDidLoad(), add the following code:

func addSwiftUIView() {
  let childView = UIHostingController(rootView: progressView)

  childView.view.translatesAutoresizingMaskIntoConstraints = false

      equalTo: loadingView.safeAreaLayoutGuide.centerXAnchor),
      equalTo: loadingView.safeAreaLayoutGuide.centerYAnchor)

  childView.didMove(toParent: self)

This instantiates a brand new UIHostingController with your progressView as the root view.

You’re adding programmatic constraints here because it’s a SwiftUI view inside a hosting controller. Of course, adding this hosting controller within the storyboard is possible, but adding it here is easier.

Finally, you need to add the SwiftUI view. You’ll do this by calling the new addSwiftUIView().

Add the following code within viewDidLoad(), right after super.viewDidLoad():


Build and run. Tap the third tab, Pines.

Indeterminate spinner showing in SwiftUI

You now see your SwiftUI ProgressView in action.

Dismissing LoadingView Upon Download

Your final goal is to dismiss loadingView once the download finishes. You can make the code more readable by wrapping duplicated code in easy-to-read functions.

Open ThirdViewController.swift. Directly below addSwiftUIView(), add the following code:

private func showSpinner() {
  loadingView.isHidden = false

private func hideSpinner() {
  loadingView.isHidden = true

In loadImage(with:), add the following code directly below self.imageView.image = UIImage(data: data):


This will hide the spinner once the image is loaded.

Finally, at the bottom of addSwiftUIView(), add the following code:


When the view is loaded, you’ll start showing the spinner.

Build and run. Tap the third tab, Pines.

The spinner now displays when your download starts and disappears when it’s done.

Loading View disappears when the download completes

Congratulations on making it this far in the tutorial. You now know how to make a spinner in SwiftUI, but you have one thing left to do before you can call yourself a progress indicator pro. That’s creating a progress bar in SwiftUI.

Integrating ProgressView With LinearProgressViewStyle in SwiftUI

In the last part of the tutorial, you’ll build a determinate ProgressView using SwiftUI. It’s more complex than the previous parts of this tutorial, so you’ll only learn the basics.

When you build a SwiftUI view, it’s completely isolated in the UI. This follows the strict Model-View-View Model design pattern, where you isolate the Views from the business logic by containing the logic within view model objects.

You’ll notice that the starter project has a SwiftUI group. Within it, there are two classes: DataModel and Download. Explore these files.

Essentially, they contain objects that you’ll inject into the SwiftUI view. They’ll monitor changes from the download task of the image. This means that you can use the DataModel to track the download progress and update your ProgressView accordingly.

Creating a ProgressView With LinearProgressViewStyle

Now that you’re familiar with the two helper classes included in the starter project, you can start working on a SwiftUI progress bar.

Open Views within the SwiftUI group.

Right-click the group and click New File…. Click SwiftUI View and then Next.

The icon to click to create a new SwiftUI view

Call this file RWLinearProgressView and click Create.

As before, you’ve created an empty SwiftUI view with a Hello, World! text object.

Directly above body, add the following:

@StateObject var dataModel = DataModel()

This instantiates a data model object, enabling you to monitor changes in the download task.

Next, replace the contents of body with the following code:

ZStack {
  VStack {
    ProgressView("Downloading...", value: dataModel.progress, total: 1)
      .progressViewStyle(LinearProgressViewStyle(tint: .blue))
      .frame(width: 150)
      .onAppear {
        guard let url = URL(string:
        else { return }
        dataModel.download(with: url)
  if let image = dataModel.image {
    Image(uiImage: image)

This code creates a brand-new ProgressView. This time, you’re using the LinearProgressViewStyle which will give you a progress view that looks like a bar and not a spinner.

You set the progress bar’s progress from your data model. Because you marked the data model as a StateObject, whenever its progress property changes, your view will update automatically. Amazing!

Finally, onAppear tells the data model to start the image download as soon as the view appears. If there is an image to display, it appears

Hosting SwiftUI ProgressView in UIKit

Now, you need to instantiate your new SwiftUI view in the fourth tab’s view controller.

Open FourthViewController.swift. At the top of the class, add the following code:

private var linearProgressView = RWLinearProgressView()

This code instantiates your new SwiftUI view. It’s time add it to the view.

Directly below viewDidLoad(), add the following code:

private func addSwiftUIView() {
  let childView = UIHostingController(rootView: linearProgressView)

  childView.view.translatesAutoresizingMaskIntoConstraints = false

      equalTo: view.safeAreaLayoutGuide.topAnchor),
      equalTo: view.safeAreaLayoutGuide.bottomAnchor),
      equalTo: view.safeAreaLayoutGuide.leadingAnchor),
      equalTo: view.safeAreaLayoutGuide.trailingAnchor)

  childView.didMove(toParent: self)

You’ve created a new UIHostingController with your SwiftUI view as the root view and set the constraints to center the view.

At the bottom of viewDidLoad(), add the following code:


Here, you call this function on view loading and add the SwiftUI view as a child view.

Build and run. Tap the fourth TabBar item, Spider. You’ll see your determinate SwiftUI ProgressView works just like you’d expect.

Progress bar under the word Downloading.

Congratulations, you’ve just built your first determinate SwiftUI progress view.

Where to Go From Here?

Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

Now you know how to add a loading indicator to your iOS app! It doesn’t take much effort and it’s fundamental to a good user experience, giving your users insight into what’s happening while using your app.

When you build, consider all the options you have for indicator views. You can use:

You also learned that you can use SwiftUI’s ProgressView in your UIKit app.

If you want to know more about making network requests and dealing with data in your app, check out the iOS Data and Networking learning path.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below.


More like this