Home > Mobile >  SwiftUI ScrollView gesture recogniser
SwiftUI ScrollView gesture recogniser

Time:09-14

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

CodePudding user response:

Using ScrollViewStyle:

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