Pull to refresh is a common UI pattern, supported in UIKit via UIRefreshControl. (Un)surprisingly, it's also unavailable in SwiftUI prior to version 3, and even then it's a bit lackluster.
This package contains a component - RefreshableScrollView
- that enables this functionality with any ScrollView
. It also doesn't rely on UIViewRepresentable
, and works with any iOS version. The end result looks like this:
- Works on any
ScrollView
. - Customizable progress indicator, with a default
RefreshActivityIndicator
spinner that works on any SwiftUI version. - Specify refresh operation and choose when it ends.
- Support for Swift 5.5
async
blocks. - Compatibility
refreshCompat
modifier to deliver a drop-in replacement for iOS 15refreshable
. - Built-in haptic feedback, just like regular
List
withrefreshable
has. - Additional optional customizations:
showsIndicators
to allow for showing/hidingScrollView
indicators.loadingViewBackgroundColor
to specify the background color of the progress indicator.threshold
that indicates how much does the user how to pull before triggering refresh.
This component is distrubuted as a Swift package. Just add this URL to your package list:
https://github.com/globulus/swiftui-pull-to-refresh
You can also use CocoaPods:
pod 'SwiftUI-Pull-To-Refresh', '~> 1.1.6'
struct TestView: View {
@State private var now = Date()
var body: some View {
RefreshableScrollView(onRefresh: { done in
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.now = Date()
done()
}
}) {
VStack {
ForEach(1..<20) {
Text("\(Calendar.current.date(byAdding: .hour, value: $0, to: now)!)")
.padding(.bottom, 10)
}
}.padding()
}
}
}
}
RefreshableScrollView(onRefresh: { done in
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.now = Date()
done()
}
},
progress: { state in // HERE
if state == .waiting {
Text("Pull me down...")
} else if state == .primed {
Text("Now release!")
} else {
Text("Working...")
}
}) {
VStack {
ForEach(1..<20) {
Text("\(Calendar.current.date(byAdding: .hour, value: $0, to: now)!)")
.padding(.bottom, 10)
}
}.padding()
}
RefreshableScrollView(action: { // HERE
await Task.sleep(3_000_000_000)
now = Date()
}, progress: { state in
RefreshActivityIndicator(isAnimating: state == .loading) {
$0.hidesWhenStopped = false
}
}) {
VStack {
ForEach(1..<20) {
Text("\(Calendar.current.date(byAdding: .hour, value: $0, to: now)!)")
.padding(.bottom, 10)
}
}.padding()
}
}
VStack {
ForEach(1..<20) {
Text("\(Calendar.current.date(byAdding: .hour, value: $0, to: now)!)")
.padding(.bottom, 10)
}
}
.refreshableCompat { done in // HERE
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.now = Date()
done()
}
} progress: { state in
RefreshActivityIndicator(isAnimating: state == .loading) {
$0.hidesWhenStopped = false
}
}
Check out this recipe for in-depth description of the component and its code. Check out SwiftUIRecipes.com for more SwiftUI recipes!
- 1.1.6 - Fixed issue where content wouldn't swipe up while in refresh state.
- 1.1.5 - Added smooth animation when loading pull is released.
- 1.1.4 - Added
threshold
andloadingViewBackgroundColor
customizations. - 1.1.3 - Add haptic feedback & increase offset a bit to fix indicator being visible on certain iPad Pro models.
- 1.1.2 - Increase offset to fix UI bug occurring on iPhones without notch.
- 1.1.1 - Added
showsIndicators
to allow for showing/hidingScrollView
indicators. - 1.1.0 - Added ability to specify custom progress view, iOS 15 support, async block support and compatibility mode.
- 1.0.0 - Initial release.