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:
ScrollView
.RefreshActivityIndicator
spinner that works on any SwiftUI version.async
blocks.refreshCompat
modifier to deliver a drop-in replacement for iOS 15 refreshable
.List
with refreshable
has.showsIndicators
to allow for showing/hiding ScrollView
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.9'
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
try? await Task.sleep(nanoseconds: 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!
threshold
and loadingViewBackgroundColor
customizations.showsIndicators
to allow for showing/hiding ScrollView
indicators.link |
Stars: 256 |
Last commit: 1 year ago |
Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics