Skip to content

Instantly share code, notes, and snippets.

@jonahaung
Created January 12, 2022 08:21
Show Gist options
  • Save jonahaung/501cc6fe259026f420d37c5b31fdae58 to your computer and use it in GitHub Desktop.
Save jonahaung/501cc6fe259026f420d37c5b31fdae58 to your computer and use it in GitHub Desktop.
import SwiftUI
private struct OffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}
}
struct CustomScrollView<Content: View>: View {
private let content: () -> Content
@Environment(\.refresh) var refreshAction: RefreshAction?
@StateObject private var manager = CustomScrollViewManager()
@State private var isRefreshing = false
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var body: some View {
GeometryReader { geometry in
ScrollView(showsIndicators: true) {
VStack {
refresher()
content()
}
.padding(.bottom, 50)
.background(Color.groupedTableViewBackground.frame(height: 99999999))
.anchorPreference(key: OffsetPreferenceKey.self, value: .top) {
geometry[$0].y
}
}
.onPreferenceChange(OffsetPreferenceKey.self) {
if refreshAction != nil, !isRefreshing, manager.canRefresh(for: $0) {
Task {
isRefreshing = true
await refreshAction?()
manager.isStarting = false
isRefreshing = false
}
}
}
}
}
private func refresher() -> some View {
Group {
if manager.isStarting {
if isRefreshing {
ProgressView()
.padding(.top)
}else {
Image(systemName: "circlebadge.fill")
.padding(.top)
.foregroundColor(.secondary)
}
}
}
}
}
class CustomScrollViewManager: ObservableObject {
enum Direction {
case top, bottom, stop
}
private let threshold: CGFloat = 150
private let limit = 30
private var counter = 0
private var scrollDirection = Direction.stop
private var progress = CGFloat.zero
var isStarting = false {
willSet {
guard newValue != isStarting else { return }
withAnimation(.interactiveSpring()) {
objectWillChange.send()
}
}
}
func canRefresh(for value: CGFloat) -> Bool {
let newProgress = value.rounded()
if isStarting {
if newProgress < 2 {
return true
}
return false
}else if newProgress > threshold {
isStarting = true
Vibration.medium.vibrate()
}else {
isStarting = false
progress = newProgress
}
return false
}
func changedDirection(for value: CGFloat) -> Direction? {
let oldProgress = self.progress
let newProgress = value.rounded()
let difference = abs(abs(newProgress) - abs(oldProgress))
let newDirection = difference < 3 ? CustomScrollViewManager.Direction.stop : (newProgress > oldProgress ? CustomScrollViewManager.Direction.top : .bottom)
if isStableDirection(newDirection: newDirection) {
return newDirection
}
scrollDirection = newDirection
return nil
}
private func isStableDirection(newDirection: Direction) -> Bool {
if scrollDirection == newDirection {
counter += 1
} else {
counter = 0
}
return counter == limit
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment