sábado, 28 de abril de 2012

Tweaking UIScrollView (I): restrict touch event handling

Sometimes you want to get the same swiping, panning, paging, bouncing or deceleration behaviors that UIScrollView provides but it's not a direct fit for your needs. I'm starting a series of posts about how to tweak UIScrollView to get some of its standard system-wide behaviors but discard the ones you don't want.

In this first article of the series, we will discuss how to restrict touch event handling in a UIScrollView to one of its subviews. This could be useful in some situations... if you want to put a partially hidden view the user can drag to fully show it, for example a hidden side menu; you can do it "by hand" with gesture recognizers, or get advantage of UIScrollView behaviors.

In practice


Let's assume you want to add a menu to your app, but in order to get the most from your screen real estate you want it to be hidden at the bottom of the screen. To reveal the menu, the user will drag or swipe up from a little menu tab; to hide the menu, drag or swipe down.

The UIScrollView approach to this problem is really simple: create a UIScrollView where the menu will be inserted as subview. Place the menu accordingly into the scroll view and set the scroll view content size. You can download a sample project from https://bitbucket.org/javieralonso/uiscrollview-tweaks/src

Execute the sample project: when you drag up the menu, it goes up, but it also goes up if you pan anywhere in the scroll view. This is a bit annoying, and prevents the user from interacting from content below the scroll view: the menu should move only when the user touches over it.

But, how to?

In Cocoa Touch, every time user touches the screen the Application window tries to know which view should receive the touch event. To do this, UIView implements the following selectors:

pointInside:withEvent:
hitTest:withEvent:

First selector asks a view if a given point falls within the view. The second one, is a recursive selector to ask a view for its farthest descendant in the view hierarchy (including itself) that contains a specified point. Thus, the only thing we have to do is to subclass UIScrollView and override pointInside:withEvent: to return YES if, and only if, any of its subviews returns YES:

@interface JAFirstTweakScrollView : UIScrollView

@end

@implementation JAFirstTweakScrollView

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    for (UIView *subview in self.subviews) {
        if ([subview pointInside:[self convertPoint:point toView:subview] withEvent:event]) {
            return YES;
        }
    }
    return NO;
}

@end

You can try this in the sample project from the previous link. Simply open ViewController.xib and change the class for the ScrollView from the default UIScrollView to JAFirstTweakScrollView:


That's it. Now, you can benefit from standard UIScrollView behavior: consistent scroll sensitivity, swipe speed, scroll bounce, etc. You can also enable paging in your scroll view; do it on the sample project and see how it works.