Mastering Pull to Refresh and Refreshable in SwiftUI

Share on facebook
Share on twitter
Share on pinterest

Since iOS 15, Apple allows us to add pull to refresh functionality to our SwiftUI apps with just a few steps. In this article, you will learn how to add pull to refresh functionality to Lists, how to use the .refreshable modifier to perform asynchronous tasks such as network requests and how to make your own custom views refreshable.

Making a List refreshable 🔄

To start with a simple example, let’s create a List that shows ten random emojis.

import SwiftUI

struct EmojiList: View {
    
    @State var emojiSet = [String]()
    
    var body: some View {
        NavigationView {
            List(emojiSet, id: \.self) { emoji in
                Text(emoji)
            }
                .navigationTitle("Random Emojis")
        }
    }
}

To fill our emojiSet with random emojis, we create a function like this:

struct EmojiList: View {
    
    @State var emojiSet = [String]()
    
    var body: some View {
        //...
    }
    
    func createRandomEmojiSet() -> [String] {
        
        var emojiSet = [String]()
        
        for _ in 1...10 {
            let range = [UInt32](0x1F601...0x1F64F)
            let ascii = range[Int(drand48() * (Double(range.count)))]
            let emoji = UnicodeScalar(ascii)?.description
            emojiSet.append(emoji!)
        }
        
        return emojiSet
    }
}

We want to fill up the emojiSet already the first time our EmojiView appears. For this we simply use the .onAppear modifier.

var body: some View {
    NavigationView {
        List(emojiSet, id: \.self) { emoji in
            Text(emoji)
        }
            .navigationTitle("Random Emojis")
            .onAppear {
                emojiSet = createRandomEmojiSet()
            }
    }
}

Our preview should now look something like this:

We want to allow the user to pull down the list to generate ten new emojis and display them in a refreshed List. 

To add the pull to refresh functionality to our SwiftUI List, simply use the .refreshable modifier. 

List(emojiSet, id: \.self) { emoji in
    Text(emoji)
}
    .refreshable {
    
    }
    //...

However, the SwiftUI pull to refresh functionality only works with Lists (we’ll look at how to make views other than Lists refreshable later).

The .refreshable modifier does the hard work for us and adds the pull gesture and the corresponding spinning activity indicator to our List.

Inside the closure of the .refreshable modifier, we can specify what should happen when the user performs the pull gesture.

In our example, we use the createRandomEmojiSet function to refill our emojiSet and display new random emojis in the List.

List(emojiSet, id: \.self) { emoji in
    Text(emoji)
}
    .refreshable {
        emojiSet = createRandomEmojiSet()
    }
    //...

Now when we do the pull gesture our List will refresh and show us new random emojis!

Pull to Refresh and Networking Requests 🌎

Okay, let’s dive a little deeper into the SwiftUI pull to refresh functionality, exploring how we incorporate networking requests into our pull to refresh workflow.

The following view shows an (initially empty) List of different US states. 

struct RandomStates: View {
    
    @State var states = ["Pull 2 get random states"]
    
    var body: some View {
        NavigationView {
            List(states, id: \.self) { state in
                Text(state)
            }
                .navigationTitle("Random US States 🇺🇸")
        }
    }
}

Unlike the previous example, however, we don’t want to generate these randomly, but rather get them from a web server. At this point, we want to choose an example as simple as possible, so I decided to use the service names.drycodes.com, which is perfect for downloading sample data for educational purposes.

Using this service we can download random states using an appropriate URL in JSON format. The URL “http://names.drycodes.com/10?nameOptions=states&combine=1&separator=space” returned about 10 random states in JSON format. 

The corresponding JSON looks something like this:

To make the JSON usable for our purposes, we need to parse it. For this we need the corresponding data model, which we add to our RandomStates.swift file:

struct StateItem: Decodable, Identifiable {
    let id: Int
    let name: String
}

At this point I won’t go further into how to use JSON in Swift, we have a separate tutorial on that here.

Hint: You may have noticed that we are only dealing with an http and not an https internet address, which means that the data to be transmitted is not encrypted. This does not bother us for our purposes, but unencrypted web traffic must be explicitly allowed. Therefore we add the “App Transport Security Settings” key to our projects Target Properties and set “Allow Arbitrary Loads” to “YES”:

Next, we add a function to our RandomStates struct that downloads the JSON data using a URLRequest and assigns the contained state names to our states array using the data model we defined earlier.

struct RandomStates: View {
    
    @State var states = ["Pull 2 get random states"]
    
    var body: some View {
        //...
    }
    
    func fetchRandomStates() {
        let url = URL(string: "http://names.drycodes.com/10?nameOptions=states&combine=1&separator=space")!
        let request = URLRequest(url: url)
        let (data, _) = try! await URLSession.shared.data(for: request)
        let fetchedStates = try! JSONDecoder().decode([String].self, from: data)
        states = fetchedStates
    }
}

Now let’s use the .refreshable modifier as before to call the fetchRandomStates function.

List(states, id: \.self) { state in
    Text(state)
}
    .refreshable {
        fetchRandomStates()
    }
    //...

However, we now get an error message: “‘async’ call in a function that does not support concurrency”.

What does this mean? 🤔

A short excursion into Swift Concurrency 💡

For this purpose, we need to understand the difference between synchronous and asynchronous functions first.

The system of the device our app runs on uses so-called threads to perform various processes, i.e. to perform a function defined by us. You can imagine threads as working units of the system.

A “normal” function is a synchronous function. A synchronous function blocks the thread until it finishes its work. As long as the thread is blocked, it cannot perform any other task. So a synchronous function is only suitable for fast executable and lightweight tasks.

An asynchronous function, on the other hand, does not block the thread. An asynchronous function can suspend, meaning giving control of the thread to the system that decides what is most important (called awaiting). At some time the system will resume the function (called resuming). This is especially useful for more complex tasks that may take a long time to complete. These include, in particular, networking processes whose speed depends on Internet performance or resource-intensive computations.

One such task is our URLSession. We tell Swift to perform the URLSession asynchronously by marking it with the await keyword. However, this is only possible in an asynchronous context. This means that our fetchRandomStates function must also be marked as asynchronous. And to do this, we mark our function as asynchronously using the async keyword.

func fetchRandomStates() async {
    //...
}

If we want to call our asynchronous fetchRandomStates function, we need to embed it into an asynchronous context. However, the .refreshable modifier already provides us with such an asynchronous environment. Therefore, we only need to use the await keyword to call the fetchRandomStates function.

.refreshable {
    await fetchRandomStates()
}

If we now start a Live preview we can see that we can use the .refreshable modifier to execute a network request and display the resulting data in our List.

Fetching updates using Pull to Refresh ⬇️

Okay, let’s strengthen our knowledge and turn to a more practical example.

The following “ExchangeRateView” is supposed to display different exchange rates, for example: “US Dollar : Jap. Yen” or “Euro : Chin. Yuan”.

import SwiftUI

struct ExchangeRateView: View {
    
    @State var exchangeRateUSDtoYEN = ""
    @State var exchangeRateEURtoCNH = ""
    
    var body: some View {
        NavigationView {
            List {
                ExchangeRateListRow(concurrency: "US-Dollar : Jap. Yen", symbol: "USD", exchangeRate: $exchangeRateUSDtoYEN)
                ExchangeRateListRow(concurrency: "Euro : Chin. Yuan", symbol: "EUR", exchangeRate: $exchangeRateEURtoCNH)
            }
                .listStyle(.grouped)
                .navigationTitle("Forex Rates 💹")
        }
    }
}

struct ExchangeRateListRow: View {
    
    let concurrency: String
    let symbol: String
    
    @Binding var exchangeRate: String
    
    var body: some View {
        HStack {
            Text(concurrency)
                .font(.headline)
            Spacer()
            VStack(alignment: .leading) {
                if let roundedExchangeRate = Float(exchangeRate) {
                    Text("1 \(symbol) =")
                        .font(.footnote)
                        .opacity(0.3)
                    Text(String(format:"%.2f", roundedExchangeRate))
                        .font(.headline)
                        .foregroundColor(.green)
                } else {
                    Text("...")
                        .opacity(0.5)
                }
            }
        }
            .frame(height: 50)
    }
}

struct ExchangeRateView_Previews: PreviewProvider {
    static var previews: some View {
        ExchangeRateView()
    }
}

The corresponding preview looks something like this:

To download the corresponding data we use the free AlphaVantageAPI. You can register for free to get your personal API key. As soon as you have it, store it in a corresponding constant.

let apiKey = "*YOUR APIKEY*"

If you want to learn more about processing and displaying financial data using the AlphavantageAPI and SwiftUI, take a look at our Mastering SwiftUI Interactive Book. In it, we also look in particular at how we can display beautiful graphs of price trends.

For example, the URL “https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=USD&to_currency=JPY&apikey=*YOURAPIKEY*” returns the following JSON:

In order to parse this, we need a suitable data model which we add to our ExchangeRateView.swift file.

struct ExchangeRate: Codable {
    let realtimeCurrencyExchangeRate: RealtimeCurrencyExchangeRate

    enum CodingKeys: String, CodingKey {
        case realtimeCurrencyExchangeRate = "Realtime Currency Exchange Rate"
    }
}

struct RealtimeCurrencyExchangeRate: Codable {
    let exchangeRate: String

    enum CodingKeys: String, CodingKey {
        case exchangeRate = "5. Exchange Rate"
    }
}

As before, we can now add an asynchronous function to our ExchangeRateView that returns the exchange rate using a URLRequest.

struct ExchangeRateView: View {
    
    @State var exchangeRateUSDtoYEN = ""
    @State var exchangeRateEURtoCNH = ""
    
    var body: some View {
        //..
    }
    
    func getExchangeRate(apiCallString: String) async -> String {
        let url = URL(string: apiCallString)!
        let request = URLRequest(url: url)
        let (data, _) = try! await URLSession.shared.data(for: request)
        let parsedData = try! JSONDecoder().decode(ExchangeRate.self, from: data)
        let exchangeRate = parsedData.realtimeCurrencyExchangeRate.exchangeRate
        
        return exchangeRate
    }
}

Now we can use the .refreshable modifier again to refresh the latest exchange rates when a pull gesture is performed.

List {
    //...
}
    .refreshable {
        exchangeRateUSDtoYEN = await getExchangeRate(apiCallString: "https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=USD&to_currency=JPY&apikey=\(apiKey)")
        exchangeRateEURtoCNH = await getExchangeRate(apiCallString: "https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=EUR&to_currency=CNY&apikey=\(apiKey)")
    }
    //…

Let’s do a Live preview to see if this works.

Making custom views refreshable 💡

As already mentioned above, the SwiftUI refreshable logic can be applied to views other than Lists. To understand how this works, let’s create a slightly different version of our ExchangeRateView.

import SwiftUI

struct DetailExchangeRateView: View {
    
    let baseSymbol: String
    let outputSymbol: String
    
    @State var exchangeRate: String
    
    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                Text("1 \(baseSymbol) =")
                    .bold()
                if let roundedExchangeRate = Float(exchangeRate) {
                    Text("\(String(format:"%.2f", roundedExchangeRate)) \(outputSymbol)")
                        .font(.largeTitle)
                        .bold()
                } else {
                    Text("...")
                }
            }
                .frame(height: 50)
                .navigationBarTitle("\(baseSymbol) : \(outputSymbol) 💱")
        }
    }
 
    func getExchangeRate(apiCallString: String) async -> String {
        let url = URL(string: apiCallString)!
        let request = URLRequest(url: url)
        let (data, _) = try! await URLSession.shared.data(for: request)
        let parsedData = try! JSONDecoder().decode(ExchangeRate.self, from: data)
        let exchangeRate = parsedData.realtimeCurrencyExchangeRate.exchangeRate
        
        return exchangeRate
    }
    
}

struct DetailExchangeRateView_Previews: PreviewProvider {
    static var previews: some View {
        DetailExchangeRateView(baseSymbol: "USD", outputSymbol: "YEN", exchangeRate: "")
    }
}

The corresponding preview looks like this:

We want our new DetailExchangeRateView to show the most current exchange rate from US dollars to Japanese Yen. To retrieve the latest exchange rate we use a Button:

Let’s add one to the VStack and embed it into a Group.

VStack(spacing: 20) {
    //...
    Group {
        Button(action: {
            
        }) {
            Text("Update Rate")
        }
    }
}

Next, we outsource this Group as a separate view and name it “RateRefresher”.

struct DetailExchangeRateView: View {
    
    let baseSymbol: String
    let outputSymbol: String
    
    @State var exchangeRate: String
    
    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                //...
                RateRefresher()
            }
                //...
        }
    }

    //…
    
}


struct RateRefresher: View {
    var body: some View {
        Group {
            Button(action: {
                
            }) {
                Text("Update Rate")
            }
        }
    }
}

Now we can add the refresh logic to our outsourced RateRefresher. We do this as usual with the .refreshable modifier.

RateRefresher()
    .refreshable {
        exchangeRate = await getExchangeRate(apiCallString: "https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=USD&to_currency=JPY&apikey=\(apiKey)")
    }

To make our RateRefresher listen to the .refreshable modifier we have to use the refresh @Environment property.

struct RateRefresher: View {
    
    @Environment(\.refresh) private var refresh
    
    var body: some View {
        //...
    }
}

If we now use this property in our RateRefresher‘s Button, we can asynchronously execute the code contained in the .refreshable closure:

struct RateRefresher: View {
    
    @Environment(\.refresh) private var refresh
    
    var body: some View {
        Group {
            if let refresh = refresh {
                Button(action: {
                    Task {
                        await refresh()
                    }
                }) {
                    Text("Update Rate")
                }
            }
        }
    }
}

Let’s run a Live preview to see if this works:

While this works, we want a spinning activity indicator to be displayed instead of the Button while the exchange rate is being downloaded. To do this we add a State property to our RateRefresher for keeping track of whether new data is being downloaded. When this is true we show a simple ProgressView instead of the Button.

struct RateRefresher: View {
    
    @State var isLoading = false
    
    @Environment(\.refresh) private var refresh
    
    var body: some View {
        Group {
            if isLoading {
                ProgressView()
            } else {
                if let refresh = refresh {
                    //...
                }
            }
        }
    }
}

Once the “Update Rate” Button is clicked we set isLoading to true. Finally, when the exchange rate has been downloaded, we set isLoading back to false, so that the “Update Rate” Button appears again.

Button(action: {
    isLoading = true
    Task {
        await refresh()
        isLoading = false
    }
}) {
    Text("Update Rate")
}

If we now start a Live preview, we will see how a spinning activity indicator is displayed for the time that the latest exchange rate is downloaded.

Finally, we can use the .onAppear closure to automatically retrieve the exchange rate when opening the DetailExchangeRateView.

NavigationView {
    VStack(spacing: 20) {
        //...
    }
        //...
        .onAppear {
            Task {
                exchangeRate = await getExchangeRate(apiCallString: "https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=USD&to_currency=JPY&apikey=\(apiKey)")
            }
        }
}

To provide a better user experience replace the placeholder “…”-Text view with a ProgressView.

if let roundedExchangeRate = Float(exchangeRate) {
    Text("\(String(format:"%.2f", roundedExchangeRate)) \(outputSymbol)")
        .font(.largeTitle)
        .bold()
} else {
    ProgressView()
}

This results in the following Live preview:

Conclusion 🎊

Awesome, you’ve learned a lot about the .refreshable modifier and its use cases. In addition to the classic pull to refresh functionality, we looked at how we perform asynchronous tasks like network requests in our SwiftUI apps and how we apply refreshable logic to views other than Lists.

Here’s the GitHub repo for this project.

Did you like the tutorial and want to learn more about developing iOS apps with SwiftUI? Then make sure to check out our Interactive Mastering SwiftUI Book!

2 replies on “Mastering Pull to Refresh and Refreshable in SwiftUI”

Leave a Reply

Your email address will not be published. Required fields are marked *

small_c_popup.png

Covid-19 Forces you into quarantine?

Start Mastering swiftUI Today save your 33% discount