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.
Attach a handler with refreshable(action:)
Applying
refreshableto a view declares that its content can be refreshed and supplies the asynchronous closure to run when that happens. SwiftUI wraps this closure in aRefreshActionand installs the standard refresh control on the enclosing scroll view. Here the closure on theListsimply doescount += 1, standing in for whatever data fetch you would perform.Read the action from the environment
SwiftUI exposes the active refresh operation through the
\.refreshenvironment key, whose value is an optionalRefreshAction?. Reading it with@Environment(\.refresh) private var refreshgives a child view access to the same refresh registered by an ancestor'srefreshablemodifier, so deeply nested controls can trigger a reload without threading callbacks down by hand.Check for availability before using it
The environment value is
nilwhenever norefreshablehandler is in scope, so you unwrap it before offering a manual control. Theif let refreshbinding gates theButton("Refresh Now")on the action's presence, ensuring you only show a refresh affordance when one is actually wired up.Invoke it as an async function
RefreshActionconforms to be callable, so you run a refresh by calling the value directly —refresh()— and because the call is asynchronous youawaitit from within aTask { 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.
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.
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()
}
}