Ad

Our DNA is written in Swift
Jump

Hacking UIScrollView Gesture Recognizers

As of SDK 3.2 most of the touch handling code in stock controls has been taken out and replaced with this amazing new technology called Gesture Recognizers. This means besides of using them yourself and creating your own you can also fiddle with behaviors of standard controls if they interfere with your own gestures.

I’m currently working quite a bit on something based on UIScrollView and there I found several things that I needed to tweak.

Disabling Pinch

The first modification I did was to get around a bug in UIScrollView.  I did not actually want user zooming in my scroll view, but just use the setting of the zoomLevel to scale images without having to redraw them. And you can only set the zoomLevel to values between min and max.

So, during autorotation, I reduced the min zoom scale, set the new zoom level and then set min and max both to the new zoom. But the problem with this, as of SDK 3.2., is that if you change the min or max zoomLevel property the scroll silently adds a UIPinchGestureRecognizer to itself via the addGestureRecognizer method that now all views have.

Now that in of itself might not be a problem if it did not do anything. But as soon as you pinch the fingers together or apart then there is a bug setting the scrollview’s contentOffset to (0,0) causing my scrollview to the left and top. So I overrode the addGestureRecognizer to specifically disable the pinch detector.

If you prevent the addition instead of disabling it, you get a crash. Subclass UIScrollView and add this code to the implementation.

- (void)addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
{
	// workaround for bug: pinch recognizer gets added during rotation,
	//  causes contentOffset to be set to (0,0) on pinch
	if ([gestureRecognizer isKindOfClass:[UIPinchGestureRecognizer class]])
	{
		gestureRecognizer.enabled = NO;
	}
 
	[super addGestureRecognizer:gestureRecognizer];
}

This way effectively disables the pinching without risking any other adverse effects.

Perpendicular Swipe Detection

The second example of fiddling with the scrollview gesture recognizers came from me wanting to use vertical swipes to trigger an action while retaining the horizontal swiping for the scroll view paging.

In my view controller I had this code to add the swipe handling:

- (void)viewDidLoad
{
	[super viewDidLoad];
 
	// other init stuff
 
	// add vertical swipe detector
	UISwipeGestureRecognizer *swipeRecognizer = [[UISwipeGestureRecognizer alloc]
		initWithTarget:self action:@selector(verticalSwiped:)];
	[self.view addGestureRecognizer:swipeRecognizer];
	swipeRecognizer.direction = UISwipeGestureRecognizerDirectionUp
		 | UISwipeGestureRecognizerDirectionDown;
	swipeRecognizer.delegate = self;
	[swipeRecognizer release];
}

With the scrollview behind it it still gets to detect all kinds of swiping also if you swipe diagonally downwards. There’s an easy way to see what kinds of gestures are currently attached to a view:

for (UIGestureRecognizer *gesture in _scrollView.gestureRecognizers)
{
	[gesture requireGestureRecognizerToFail:swipeRecognizer];
	NSLog(@"%@", gesture);
}

All gesture recognizers have a good description and a paging scrollview gets these:

  • UIScrollViewDelayedTouchesBeganGestureRecognizer
  • UIPanGestureRecognizer, must-fail = UIScrollViewPagingSwipeGestureRecognizer
  • UIScrollViewPagingSwipeGestureRecognizer, must-fail-for = UIPanGestureRecognizer

The pan gesture is the stock one which we also could use, but the two others are custom recognizers that Apple made by subclassing UIGestureRecognizer and added specific detection logic. Now we wouldn’t want to mess with these, let alone even mention UIScrollViewDelayedTouchesBeganGestureRecognizer or UIScrollViewPagingSwipeGestureRecognizer in our code, because those are private API classes and would cause our app to be rejected.

But again there’s a simple fix to the dilemma.Note the dependencies must-fail and must-fail-for. In the log you see that for the pan gesture to work, the paging swipe gesture must fail first. This mechanism lets you make gestures dependent on the failure of others or themselves cause others to fail.

We can employ the same mechanism and permit the detection of the paging swipe only if our own vertical swipe gesture failed.

for (UIGestureRecognizer *gesture in _scrollView.gestureRecognizers)
{
	// don't want to mention any Apple TM'ed class names ;-)
	NSString *className = NSStringFromClass([gesture class]);
	if ([className rangeOfString:@"Swipe"].location!=NSNotFound)
	{
		[gesture requireGestureRecognizerToFail:swipeRecognizer];
	}
}

I didn’t want to mess with the two other recognizers because a) I don’t know what the delayed touches one is for and b) making the pan dependent on the failing of the vertical swipe might introduce a bit of lag for the panning to be detected. So I just got the class name from the instances and made apples paging swipe dependent on my vertical swipe detector failing.

So you see, Gesture Recognizers are not only greatly simplifying touch handling for your apps, but if you dare look at how Apple is using them you can also tweak them a bit if necessary.


Categories: Recipes

7 Comments »

  1. Thanks for this article, it just solved my problem in my UIScrollView, on how to disable the irritating pinch-to-zoom functionality.
    I don’t have any problems with the addition as-is, but I didn’t need it, and disabling seemed almost impossible.

    So, thanks for solving this for me!

    Bryan

  2. Good article !
    Did you get what is the UIScrollViewDelayedTouchesBeganGestureRecognizer now ?

  3. I kind of understand. The Scrollview waits for like 150 ms before performing any actions.

  4. Thanks for posting this, I’m using it in my latest app!

  5. Thank you very much for the article.

Trackbacks

  1. UIWebView Zoom/Pinch - iPhone Dev SDK Forum