Pull To Refresh in SwiftUI

Pull To Refresh in SwiftUI
⏱ Reading Time: 5 mins

SwiftUI gets better and better every year, and in WWDC 2021 we heard about some great new additions and improvements. One such addition that this post is focusing on is the pull to refresh control; a missing UI element familiar from the UITableView in UIKit, but not available in the first two versions of SwiftUI.

Putting the pull to refresh functionality in action is pretty easy in SwiftUI. We only have to apply a specific view modifier on a List view, and keep one or two things in mind, but crucial to the app’s proper behavior. Let’s go through them.

Using pull to refresh

Consider the following simple example, where we have a list with country names and their flags:

A list showing five countries with their flags, Italy, France, Greece, Germany, Austria.

For the sake of the demonstration here, the data is retrieved from a local, custom JSON file that contains a few European countries with URLs to their flag images. Even though there are just a few countries listed initially, the purpose is to add a new country to the top every time we pull to refresh the list.

In order to achieve that, we firstly need a source of truth for the data that we’ll present in the list:

CountriesModel is an ObservableObject type that implements all the necessary logic that makes the specific example work. There is also an auxiliary type, called CountryData that keeps the actual data for a single country; its name, the URL to the flag image, as well as a unique identifier, because it conforms to the Identifiable protocol.

Note: You can download the demo project of this post to find out all implementation details.

The following snippet creates the actual list:

CountryView is a SwiftUI view that contains an AsyncImage view and a Text view that display the data in each row.

In order to enable the pull to refresh functionality, we’ll apply a brand new view modifier to the List; it’s called refreshable():

The closure’s body is the place to call any code that will update the list’s datasource, and therefore update the displayed elements:

With this small addition only, the list can now be refreshed when pulling it downwards. Obviously, the actual operation that will update the data and will trigger a UI update is totally dependent on the app. However, the above snippet clearly demonstrates you to deal with the refreshable() view modifier.

The result of the above is shown right next:

Pull to refresh list. A new country is added to the top on each refresh.

Update on background but perform UI changes on main thread

In the “What’s new in SwiftUI” session of WWDC 2021, the demonstrated code makes use of the new concurrency tools made available this year (async/await). Of course, it’s not mandatory to use that with the refreshable() modifier, however there is one thing to keep in mind:

You should always update the data after a pull to refresh action on the background, leaving the main thread free so the app remains responsive. Use the main thread only to update the source of truth that feeds the list with the displayed data.

In order to make that more clear, let’s take a look at the implementation of the updateCountries() method used in the previous section:

What happens in the above three lines is simple:

  • At first we make sure that the allCountries array that contains all available countries loaded from JSON file is not empty, and that the pointer to the next country to show is within the proper range.
  • The next available country is added to the countries array as its first element. countries property is marked with the @Published property wrapper and constitutes the source of truth for the data displayed on the list.
  • The pointer to the next country is increased by one.

That piece of code has little real value out of the scope of this post. Disregarding that fact though, it’s obvious that this code is not being executed on any background thread. On the other hand, knowing that we have just a fistful of data already loaded into the memory, then reasonably we don’t expect any delay when updating the displayed countries.

Let’s change that, and let’s use an unorthodox way to prove the point here. Let’s add a for-in loop that will perform 1000000 repetitions before the countries array gets updated:

By running the app again now, you can see that it remains unresponsive for a few moments:

Pull to refresh causes app freezing because it's running a time consuming task on the main thread.

That (totally unnecessary) loop is running on the main thread, so the UI freezes until the execution of the above method is finished. This is a totally unaccepted behavior for the app, and a really bad user experience.

To fix that, let’s move everything into a background queue:

By running the app once again a new problem comes up:

Xcode warning message saying that publishing changes from a background thread.

What the message says is that we’re publishing updates from a background thread, but that’s something not allowed. countries property, marked with the @Published property, is a publisher and the source of truth for the list’s data. So, in practical words, countries should be updated and therefore publish new values on the main thread; always.

With that in mind, it’s necessary to include the execution of the last two lines in the main thread:

This time the list is refreshed properly. See next that even though there is a small delay until the next country appears (because of the loop), the UI remains totally responsive.

Properly running pull to refresh with time consuming task to be executed on the background and UI updates on the main thread.

Summary

Adding a pull to refresh control to a SwiftUI list in order to update the displayed data is pretty simple. However, it’s important to remember that the update should not block the main thread and the UI under any circumstances. That’s something that I hope I managed to showcase through the simple, and probably not so elegant example above, but undoubtedly into the point. In any case, thanks for reading!

Note: You may download the demo project from this link.

This post has also been published on Medium.

If you found this post useful then please consider sharing it! Also, subscribe to my newsletter in order to be notified about everything new published here directly in your inbox, and follow me on Twitter, on YouTube, on Medium and other social media.