How do I add pull-to-refresh to lists in Combine with Swift?

Implementing pull-to-refresh in SwiftUI lists with Combine allows for a seamless user experience when fetching new data. Below is an example of how to achieve this functionality using Swift.

import SwiftUI import Combine struct ContentView: View { @State private var items: [String] = [] @State private var isRefreshing = false private var cancellable: AnyCancellable? var body: some View { List { ForEach(items, id: \.self) { item in Text(item) } } .pullToRefresh(isShowing: $isRefreshing) { fetchData() } .onAppear { fetchData() } } private func fetchData() { isRefreshing = true cancellable = Just([1, 2, 3, 4, 5].map { "Item \($0)" }) .delay(for: .seconds(2), scheduler: RunLoop.main) .sink(receiveCompletion: { _ in isRefreshing = false }, receiveValue: { newItems in items = newItems }) } } extension View { func pullToRefresh(isShowing: Binding, action: @escaping () -> Void) -> some View { self.modifier(PullToRefresh(isShowing: isShowing, action: action)) } } struct PullToRefresh: ViewModifier { @Binding var isShowing: Bool let action: () -> Void func body(content: Content) -> some View { content .background(GeometryReader { geometry in Color.clear.preference(key: ScrollOffsetKey.self, value: geometry.frame(in: .global).minY) }) .onPreferenceChange(ScrollOffsetKey.self) { offset in if offset > 100 && !isShowing { action() } } } } struct ScrollOffsetKey: PreferenceKey { typealias Value = CGFloat static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } }

Swift Combine SwiftUI pull-to-refresh List