How can I detect when a ScrollView is being dragged?
Within my ScrollView I have an @Binding scrollPositionOffset variable that I watch with .onChange(of:) and then programmatically scroll to that position using ScrollViewReader.scrollTo(). This works great, but I need to also update scrollPositionOffset when I scroll the ScrollView directly. I'm struggling to do that as this would trigger the .onChange(of:) closure and get into a loop.
My solution is to conditionally call ScrollViewReader.scrollTo() only when I have a localScrolling variable set to false. I've tried to set this using DragGesture.onChanged and .onEnded, but this isn't the same as the drag gesture that causes the scroll, so .onEnded never fires.
What I think I need is a @GestureRecognizer for ScrollView similar to UIScrollView's isDragging or isTracking (I'm aware I could use UIScrollView, but I don't know how, and that seems like it might be more work!! I'd accept an answer that shows me how to drop that into a SwiftUIView too)
Context (in case anyone has a cleaner solution to my actual scenario):
I have a ScrollView that I'm programmatically scrolling to create an effect like the Minimap view within Xcode (i.e. I have a zoomed-out view adjacent to the ScrollView, and dragging the minimap causes the ScrollView to scroll).
This works great when I use the minimap, but I'm struggling to get the reverse to happen: moving the position of the ScrollView to update the minimap view.
Code
@Binding var scrollPositionOffset: CGFloat
let zoomMultiplier:CGFloat = 1.5
var body: some View{
ScrollViewReader { scrollViewProxy in
GeometryReader{ geometry in
ScrollView {
ZStack(alignment:.top){
//The content of my ScrollView
MagnifierView()
.frame(height: geometry.size.height * zoomMultiplier)
//I'm using this as my offset reference
Rectangle()
.frame(height:10)
.alignmentGuide(.top) { _ in
geometry.size.height * zoomMultiplier * -scrollPositionOffset
}
.id("scrollOffset")
}
}
.onAppear(){
scrollViewProxy.scrollTo("scrollOffset", anchor: .top)
}
.onChange(of: scrollPositionOffset, perform: { _ in
//Only call .scrollTo() if the view isn't already being scrolled by the user
if !localScrolling {
scrollViewProxy.scrollTo("scrollOffset", anchor: .top)
}
})
.gesture(
DragGesture()
.onChanged{gesture in
localScrolling = true
let offset = gesture.location.y/(zoomMultiplier * geometry.size.height)
scrollPositionOffset = offset
}
.onEnded({gesture in
//Doesn't ever fire when scrolling
localScrolling = false
})
)
}
}
}
Using ScrollUI:
struct CustomScrollView: ScrollViewStyle {
@Binding var isDragging: Bool
func make(body: AnyView, context: Context) -> some View {
body
}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
class Coordinator: ScrollViewCoordinator {
var parent: CustomScrollView
init(parent: CustomScrollView) {
self.parent = parent
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
parent.isDragging = false
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
parent.isDragging = true
}
}
}
struct TestView: View {
@State var isDragging = false
var body: some View {
ScrollView {
}.scrollViewStyle(CustomScrollView(isDragging: $isDragging))
}
}
Update:
I've update the package's api to match the native one provided by Apple in iOS 18.
You can use the onScrollStateChange modifier to track whether the ScrollView is being scrolled:
struct TestView: View {
@State private var isScrolling = false
var body: some View {
ScrollView {
...
}.onScrollStateChange { oldState, newState, context in
isScrolling = newState.isScrolling
}.scrollViewStyle(.default)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With