TechnologiesSwiftUISearch and Find

RefreshAction struct

iOSmacOStvOSwatchOSvisionOSiOS 15.0+✓ renders

An action that initiates a refresh operation.

How it works

RefreshAction is a structure that encapsulates the operation SwiftUI runs when the user asks to reload a view's content, typically by pulling down a scrollable list. You rarely construct one yourself; instead you register a refresh handler with the refreshable(action:) modifier, and SwiftUI publishes the resulting action into the environment under the refresh key. Reach for it when a view needs an on-demand way to fetch fresh data and you want the system to manage the standard pull-to-refresh affordance, progress indicator, and concurrency for you. Because the action is async, it naturally cooperates with structured concurrency and keeps the refresh gesture alive until your work finishes.

  1. Attach a handler with refreshable(action:)

    Applying refreshable to a view declares that its content can be refreshed and supplies the asynchronous closure to run when that happens. SwiftUI wraps this closure in a RefreshAction and installs the standard refresh control on the enclosing scroll view. Here the closure on the List simply does count += 1, standing in for whatever data fetch you would perform.

  2. Read the action from the environment

    SwiftUI exposes the active refresh operation through the \.refresh environment key, whose value is an optional RefreshAction?. Reading it with @Environment(\.refresh) private var refresh gives a child view access to the same refresh registered by an ancestor's refreshable modifier, so deeply nested controls can trigger a reload without threading callbacks down by hand.

  3. Check for availability before using it

    The environment value is nil whenever no refreshable handler is in scope, so you unwrap it before offering a manual control. The if let refresh binding gates the Button("Refresh Now") on the action's presence, ensuring you only show a refresh affordance when one is actually wired up.

  4. Invoke it as an async function

    RefreshAction conforms to be callable, so you run a refresh by calling the value directly — refresh() — and because the call is asynchronous you await it from within a Task { await refresh() }. SwiftUI keeps the refresh indicator visible for the lifetime of that call, so the longer your handler runs, the longer the spinner stays on screen.

Try it — Make the refreshable closure sleep before mutating state — e.g. try? await Task.sleep(for: .seconds(2)); count += 1 — to watch the refresh indicator persist for the full duration of the RefreshAction call.

Example & preview

Press Run live & edit to compile it in your browser — then edit the Swift on the left and the preview re-renders live.

RefreshAction.swift
struct RefreshActionDemo: View {
    @Environment(\.refresh) private var refresh
    @State private var count = 3

    var body: some View {
        List {
            ForEach(0..<count, id: \.self) { i in
                Text("Item \(i + 1)")
            }
            if let refresh {
                Button("Refresh Now") {
                    Task { await refresh() }
                }
            }
        }
        .refreshable {
            count += 1
        }
        .padding()
    }
}
Live preview
Item 1 Item 2 Item 3 Refresh Now
Item 1 Item 2 Item 3 Refresh Now
swift → lexer → parser → sema → uiir → canvas Open in Studio ↗
What's new in SwiftUI 27 →