SwiftUI: Pull-to-Refresh

Part of a series of blogs about SwiftUI.


Starting in iOS 15, we are now able to take advantage of native pull-to-refresh behavior in SwiftUI with refreshable(action:). This is quite nice, as it handles most everything for us automatically, including presentation and dismissal of the activity indicator - a common source of UI bugs in the past.

If we look at the documentation, we see:

A view with a new refresh action in its environment.

It takes one parameter, action, of type @escaping () async -> Void:

An asynchronous handler that SwiftUI executes when the user requests a refresh. Use this handler to initiate an update of model data displayed in the modified view. Use await in front of any asynchronous calls inside the handler.

We can use this to trigger updating or refreshing the view! It's really easy to use. Just how easy? We'll whip up a pull-to-refresh list view in a jiffy!

We'll start with the supporting structures:

struct PullRefreshItem: Identifiable {
    let id: UUID = UUID()
    let string: String

We've got to make it Identifiable because we're going to be using it as part of our view state.

Next, we have our view itself:

struct PullRefreshView: View {
    @State var items: [PullRefreshItem] = [ "Foo", "Bar", "Qux" ]
        .map { PullRefreshItem(string: $0) }

    var body: some View {
        List(items) { item in
        .refreshable {
            do {
                print("Simulating refresh fetch")
                try await Task.sleep(.seconds(1))
                items = items + ["Zip","Zap","Bap"]
                    .map { PullRefreshItem(string: $0) }
            } catch {
                // Cancelled or unhandleable error

For completion, finally we have the preview provider:

struct PullRefreshView_Previews: PreviewProvider {
    static var previews: some View {

If we look at our PullRefreshView class, it really is quite simple. We have a small @State variable for our array of items, and the body is just a List of Text displaying the item.string. The magic that we're interested in today is where we call refreshable(action:) on the list, and now whenever we pull down on the list, it will update for us using that action.

In this example case, it will just keep adding "Zip" "Zap" and "Bap" to the top of the list every time you do it, but replacing that with a URLSessionDataTask to actually fetch data isn't much more difficult.

That's all there is to it!