History
Loading...
Loading...
October 2, 2025
.refreshable modifier on any scrollable view to add native pull-to-refresh functionality. It automatically handles the visual feedback and gesture recognition, triggering your async refresh logic when users pull down.This pattern separates initial loading from refresh actions, provides proper empty states, and ensures UI updates happen on the main actor. The refreshable modifier automatically handles the pull gesture and visual indicators.
@State private var items: [Item] = []
@State private var isInitialLoading = true
@State private var lastRefresh = Date()
var body: some View {
NavigationView {
Group {
if isInitialLoading {
ProgressView("Loading...")
} else {
List(items) { item in
ItemRow(item: item)
}
.refreshable {
await refreshData()
}
.overlay {
if items.isEmpty {
ContentUnavailableView(
"No Items",
systemImage: "tray",
description: Text("Pull down to refresh")
)
}
}
}
}
.navigationTitle("Items")
.task {
await loadInitialData()
}
}
}
private func refreshData() async {
do {
let newItems = try await dataService.fetchItems()
await MainActor.run {
items = newItems
lastRefresh = Date()
}
} catch {
// Handle error appropriately
}
}
private func loadInitialData() async {
await refreshData()
await MainActor.run {
isInitialLoading = false
}
}Distinguishing between initial loads and refreshes prevents awkward UX where users see pull-to-refresh indicators during first launch. Proper state management ensures smooth transitions and prevents UI glitches when data updates occur on background threads.