Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI ScrollView gesture recogniser

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
                        })
                )
            }
        }
    }

like image 582
Ben Frearson Avatar asked Oct 18 '25 21:10

Ben Frearson


1 Answers

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)
    }
}
like image 165
SwiftyJoeyy Avatar answered Oct 20 '25 13:10

SwiftyJoeyy



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!