5

I'm working on an app where I have several UIView objects that are subviews on a UIScrollView object. I create the subviews programmatically and place them on the scroll view according to the properties of associated objects. The user is allowed to move these subviews around on the scroll view. Usually this works, but sometimes the scrollview grabs the pan gesture.

What I'd like to do is to suppress the scroll view gesture recognizer if the touch location is inside one of the subviews.

I can find the scroll view gesture recognizer by looking through the scroll view's array of gesture recognizers and looking for a UIScrollViewPanGestureRecognizer object. I assume there can only be one.

An idea I have is to make my view controller be a delegate of this gesture recognizer and then have the delegate suppress it if the touch is within the bounds of one of the subviews.

Is this the best way to handle this scenario, or is there a better way?

I've done something similar, described in my answer to my own question here. How to get stepper and longpress to coexist?

Hmmm. Looks like it will be more difficult than I anticipated to recognize the scrollview's UIScrollViewPanGestureRecognizer. Any hints on doing this would be appreciated.

My idea doesn't work. In order to code my idea, I had to make my VC be the delegate of the scrollview's pan gesture recognizer. However, when I do that, I get this error:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'UIScrollView's built-in pan gesture recognizer must have its scroll view as its delegate.'

Here is the code I used. In viewDidLoad I called a method which got the scrollview's pan gesture recognizer and set self as delegate (self.scrollViewPanGestureRecognizer is just a property to store it):

self.scrollViewPanGestureRecognizer = [self.scrollView panGestureRecognizer];
self.scrollViewPanGestureRecognizer.delegate = self;

I then implemented this delegate method:

-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
   //Disable touch if touch location is in a subview.
   BOOL enableGestureRecognizer = YES;

   if (gestureRecognizer == self.scrollViewPanGestureRecognizer) {
      CGPoint touchLocation = [touch locationInView:self.scrollView];
      for (UIView *s in self.scrollView.subviews) {
         if (CGRectContainsPoint(s.frame, touchLocation)) {
            enableGestureRecognizer = NO;
         }
      }
   }
   return enableGestureRecognizer;
}

Seemed like a good idea, but it looks like I can't make my VC be the delegate.

Just tried setting scrollEnabled to NO on the scroll view. That successfully disabled scrolling, but it did not fix the problem. Views still occasionally do not respond to gestures. Thinking that perhaps some bug caused the gesture recognizer to fall off the object, I asked the debugger to display the gesture recognizers for the problematic views. They were still there. I'm stumped.

UPDATE: New information. I finally realized that the subviews that aren't responding are the ones on the right side of the screen. After carefully testing, it seems that this happens only in landscape orientation and only when the finger location is to the right of the right edge in portraite, i.e. 320 points. Apparently, something is not being handled property when rotating to landscape. Everything appears normal, but the gestures aren't being recognized.

Just for grins, I decided to display the frames and bounds and content area in the method viewDidLayoutSubviews. What I get is:

self.view.frame             is {{0, 0}, {480, 320}} 
self.view.bounds            is {{0, 0}, {480, 320}} 
self.scrollView.frame       is {{0, 0}, {480, 320}} 
self.scrollView.bounds      is {{0, 0}, {480, 320}}
self.scrollView.contentSize is {480, 320}

I seem to have missed something. What else needs to be set when rotating?

Community
  • 1
  • 1
Victor Engel
  • 2,037
  • 2
  • 25
  • 46

4 Answers4

4

use requireGestureRecognizerToFail: method.

you want your scroll view pan gesture (scrollViewGesture) to be failed when one of the gestures happen on its subView.
So, when you add pan gesture to your subView (subViewGesture), set below property as

scrollViewGesture.requireGestureRecognizerToFail =subViewGesture;
santhu
  • 4,796
  • 1
  • 21
  • 29
  • A preliminary test seems to have confirmed this to work. I need to do more testing though before accepting this answer, since the problem was just an occasional problem to begin with. – Victor Engel Feb 03 '14 at 06:32
  • One potential problem with this method is that it could introduce a delay in the scroll view's pan gesture recognizer's performance. So far, though, I don't see a significant delay. An example delay would be where a tap requires a double-tap to fail. That results in a default 0.2 second delay, I believe. Apparently, the failures I'm looking at return nearly instantly, though. – Victor Engel Feb 03 '14 at 06:40
  • After further testing, it looks like this doesn't work. – Victor Engel Feb 03 '14 at 14:30
  • I suspect the reason this doesn't work is that the method sets a relationship between two recognizers. In order for this to work, the scroll view's recognizer would need to require all the others to fail. To get it to work, I'd probably need to attach the recognizer to the scrollview instead of the subviews and fail it unless the touch location is inside one of the subviews. Now this is looking as complicated as the other offered solution. – Victor Engel Feb 03 '14 at 14:49
  • possible to `requireGestureRecognizerToFail` with multiple other gestures? or just one? – markturnip Oct 06 '16 at 07:37
  • you can use UIGestureRecogniserDelegate methods to work with multiple gestures. See this method - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0); – santhu Oct 06 '16 at 07:44
  • santhu's answer works for me in Swift 4. I have a parent scrollview that the user can pinch/zoom, and pan. Within that parent scrollview, I have a child scroll view which contains text that the user can scroll up and down. Sometimes when the user tries to scroll up / down, the parent scrollview's pan gesture happens instead. Inside the child scroll view, I did this: `parentscrollView.panGestureRecognizer.require(toFail: childScrollView.panGestureRecognizer)`, and now the user can scroll up and down on the child without the parent view gesturing being triggered instead. – SomeGuy Nov 18 '18 at 03:15
1

I found the solution. I'd forgotten that the subviews are not placed directly into the scroll view. There is a view originally occupying the bounds of scrollview onto which the subviews are placed. The hierarchy is like this:

self.view
    scroll view
        UIView (fills whole scroll view)
            subview1
            subview2
            subviewn

In my code to handle rotation, I was not resizing the UIView into which the subviews are placed. Correcting this issue solved the problem.

I'd originally tried placing the subviews without their UIView superview in between them and the scroll view, but it didn't work for some reason. Adding this extra layer solved that problem, but I forgot to handle the resizing when rotating.

So I guess the gesture recognizers did not respond because although they were visible, they were outside the bounds of their superview.

Victor Engel
  • 2,037
  • 2
  • 25
  • 46
0

I'm making this answer a community wiki because I haven't completely worked out this solution yet. The main thing is to take advantage of this from the documentation:

Subclasses can override the touchesShouldBegin:withEvent:inContentView:, pagingEnabled, and touchesShouldCancelInContentView: methods (which are called by the scroll view) to affect how the scroll view handles scrolling gestures.

Victor Engel
  • 2,037
  • 2
  • 25
  • 46
-1

One solution: instead of playing around with gesture recognizers, just disable all of them and use touchesBegan, touchesMove and touchesEnded directly. It might be a bit of work, but pretty sure it will work exactly the way you want.

You need to disable user interaction on the subviews, disable scrolling on the scrollview, and modify the scrollview's contentOffset directly.

Khanh Nguyen
  • 11,112
  • 10
  • 52
  • 65
  • I think I'm going with the other answer so I don't have to do all the work involved with this option. So far so good. – Victor Engel Feb 03 '14 at 06:37