Pan view using UIPanGestureRecognizer within a functional UIScrollView
Asked Answered
L

1

1

The Problem

I have a UIScrollView containing a UIView that I wish to allow the user to pan using a UIPanGestureRecognizer.

In order for this to work as desired, users should be able to pan the view with one finger, but also be able to pan the scroll view with another finger - doing both at the same time (using one finger for each).

However, the scroll view ceases to work when the user is panning a view contained within it. It cannot be panned until the view's pan gesture ends.

Attempted Workaround

I tried to work around this by enabling simultaneous scrolling of both the pan view and the UIScrollView that contains it by overriding the following UIGestureRecognizerDelegate method:

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    return true
}

However, this makes it so that panning the view also moves the scroll view. Each element's panning gesture should be independent of the other, not linked.

Demo Project

I have created a simple demo project that should demonstrate this, here:

https://github.com/jeffc-dev/ScrollViewPannerTest

This project contains a scroll view with a square view that should be able to be panned independently of its containing scroll view, but can not.

Why I'm Doing This

The point of this is to make it easier/quicker for a user to find a destination to pan the view to. The is somewhat analogous to rearranging icons in Springboard: You can use one finger to pan an app icon while simultaneously panning between pages with another finger, quickly finding a place to drop it. I'm not using a paged scroll view - just a normal one - and I want it to be a seamless panning gesture (I don't need/want the user to have to enter a 'wiggle mode') but the basic principle is the same.


UPDATE: DonMag helpfully came up with the idea of using a UILongPressGestureRecognizer to move the view out of the scroll view for panning, which does seem promising. However, if I went that route I think I'd need to seamlessly transition to using a UIPanGestureRecognizer after doing so (as I do use some pan gesture recognizer-specific functionality).

Longitude answered 5/8, 2021 at 9:1 Comment(0)
S
2

I'm sure there are different ways to do this, but here is one approach...

Instead of using a UIPanGesture I used a UILongPressGesture.

When the gesture begins, we move the view from the scrollView to its superview. While we continue to press the view and drag it around, it is now independent of the scrollView. When we end the gesture (lift the finger), we add the view back to the scrollView.

While dragging, we can use a second finger to scroll the content of the scroll view.

The main portion of the code looks like this:

@objc func handleLongPress(_ g: UILongPressGestureRecognizer) -> Void {
    
    switch g.state {
    
    case .began:
        
        // get our superview and its superview
        guard let sv = superview as? UIScrollView,
              let ssv = sv.superview
        else {
            return
        }
        theScrollView = sv
        theRootView = ssv
        
        // convert center coords
        let cvtCenter = theScrollView.convert(self.center, to: theRootView)
        self.center = cvtCenter
        curCenter = self.center
        
        // add self to ssv (removes self from sv)
        ssv.addSubview(self)
        
        // start wiggling anim
        startAnim()
        
        // inform the controller
        startCallback?(self)
        
    case .changed:
        
        guard let thisView = g.view else {
            return
        }
        
        // get the gesture point
        let point = g.location(in: thisView.superview)
        
        // Calculate new center position
        var newCenter = thisView.center;
        newCenter.x += point.x - curCenter.x;
        newCenter.y += point.y - curCenter.y;
        
        // Update view center
        thisView.center = newCenter
        curCenter = newCenter
        
        // inform the controller
        movedCallback?(self)
        
    default:
        
        // stop wiggle anim
        stopAnim()
        
        // convert center to scroll view (original superview) coords
        let cvtCenter = theRootView.convert(curCenter, to: theScrollView)
        
        // update center
        self.center = cvtCenter
        
        // add self back to scroll view
        theScrollView.addSubview(self)
        
        // inform the controller
        endedCallback?(self)
        
    }
    
}

I forked your GitHub repo and added a new controller to demonstrate: https://github.com/DonMag/ScrollViewPannerTest

You'll see that it is just a Starting Point for this approach. The view being dragged (actually, in this demo, you can use two fingers to drag two views at the same time) uses closures to inform the controller about the dragging...

Currently, "drag/drop" does not affect any other subviews in the scrollView. The only closure that does anything is the "ended" closure, at which point the controller re-calcs the scrollView's contentSize. The "moved" closure could be used to re-position views -- but that's another task.

Sambo answered 5/8, 2021 at 17:6 Comment(5)
Thanks, Don! I took a quick look at your solution and on the face of it it looks like it'll do what I need. My production (non-demo) app actually does something similar to this - moving the pan view to a superview of the scroll view while panning - so I think the long press gesture recognizer will be the key change for me. I'll try adapting my own code to do this and check back with what I find out. Thanks again.Longitude
Looking a bit more closely at this, it does seem that removing the UIPanGestureRecognizer entirely isn't without a cost. My simplified demo doesn't reflect this, but I do use the 'velocity' tracking in the pan gesture recognizer in my production app. One approach I've looked at is using the UILongPressGestureRecognizer to move the view out of the scroll view and have it start using the UIPanGestureRecognizer afterword, but I don't know how to seamlessly transition between the two. If you (or anyone else) has any ideas, I'd definitely be interested in hearing them.Longitude
@JeffC. -- I've played around a bit trying to use PanGesture either by itself or in addition to LongPress, without success. You may want to try implementing Velocity with LongPress on your own.Sambo
@JeffC. -- if you're still looking for a solution, I updated my GitHub repo with a velocity calculation for the UILongPressGestureRecognizer approach.Sambo
I'll give this a look when I get a chance. While I'm a little wary of straying from a pure pan gesture controller approach, if the velocity stuff works I can probably adapt it to my needs. Thanks!Longitude

© 2022 - 2024 — McMap. All rights reserved.