I implemented a custom RefreshableScrollView using PreferenceKey
import SwiftUI
/// RefreshableScrollView
public struct RefreshableScrollView<Content:View>: View {
@Binding private var isRefreshing: Bool
@State private var offset: CGFloat = 0
@State private var canRefresh: Bool = true
private var content: () -> Content
private let threshold: CGFloat = 50.0
public init(
isRefreshing: Binding<Bool>,
@ViewBuilder content: @escaping () -> Content
) {
self._isRefreshing = isRefreshing
self.content = content
}
public var body: some View {
GeometryReader { geometry in
ScrollView {
VStack(spacing: 0) {
if isRefreshing || !(canRefresh) {
ProgressView()
.progressViewStyle(
CircularProgressViewStyle(tint: Color.purple)
)
} else {
Image("arrowDown")
.resizable()
.frame(width: 22, height: 22, alignment: .center)
.foregroundColor(Color.purple)
.padding(.top, -21)
.rotationEffect(.degrees((offset < (threshold - 2)) ? 0 : 180))
.animation(.easeInOut(duration: 0.5))
}
content()
.anchorPreference(key: OffsetPreferenceKey.self, value: .top) {
geometry[$0].y
}
}
}
.onPreferenceChange(OffsetPreferenceKey.self) { offset in
DispatchQueue.main.async {
if canRefresh {
self.offset = offset
if offset > threshold && canRefresh {
withAnimation {
/// In case u need haptic feel
hapticSuccess()
isRefreshing = true
}
canRefresh = false
}
} else {
if offset < 21 {
canRefresh = true
}
}
}
}
.animation(.easeInOut, value: isRefreshing)
}
}
}
fileprivate struct OffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
public extension View {
func hapticSuccess() {
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
}
/// In case u need a modifier
func refreshable(isRefreshing: Binding<Bool>) -> some View {
modifier(RefreshableScrollViewModifier(isRefreshing: isRefreshing))
}
}