Capturing Touches On A View Inside A Scroll View

I recently needed to create a vertically scrolling UIScrollView with a child sub view on which I needed to capture touches. Working with this kind of layering of views and the responder chain can be tricky but it is not as difficult as it first appears. If you are unfamiliar with the responder chain in iOS I recommend the Apple documentation on the subject available here.

For this example lets say we have a UIScrollView that is the size of the screen, and inside of the scroll view we add a sub view; say a UIView that is 300 pixels wide by 200 pixels high. We want the scroll view to scroll vertically up and down when the user drags their finger anywhere outside of the sub view, but if the user touches inside of the sub view we want it to process the touches, not the scroll view.

The way to do this is to have the scroll view be a custom subclass of UIScrollView so that we can add some code to intercept the touches on it and thus control the behaviour as we see fit. To create the sub class in Xcode, Control-click in the Navigator and choose New File… then choose Objective-C Class and make it a sub class of UIScrollView. I named mine CustomScrollView.

In the header of the new CustomScrollView class add a CGRect property. This is what will define the area of the sub view in the scroll view. We will use this later to turn off scrolling when the touches are in our sub view. Your header should look like this:

@interface CustomScrollView : UIScrollView {
}

@property (assign) CGRect subViewRect;

@end

Now change the implementation file to be:

#import "CustomScrollView.h"

@implementation CustomScrollView

@synthesize subViewRect;

- (id)initWithFrame:(CGRect)frame 
{
  return [super initWithFrame:frame];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
   [[self nextResponder] touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{	
   [[self nextResponder] touchesMoved:touches withEvent:event];
}

- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 
{	
  [[self nextResponder] touchesEnded:touches withEvent:event];
}

@end

After your basic initWithFrame: method we add the three touch responder methods: touchesBegan:withEvent:, touchesMoved:withEvent: and touchesEnded:withEvent. Any time a touch happens on the scroll view, then these methods will be called on the CustomScrollView object as part of the responder chain. Because UIScrollView uses gesture recognizers under the hood, and UIGestureRecognizer objects are not in the responder chain, we are safe to just pass these methods on to the next responder.

That is what the method call in each of these is doing. We are telling the touches received on the scroll view to pass themselves along to the next responder which will be the controller of our sub view. In that controller that owns the sub view, wether a custom NSObject based controller or a UIViewController, you will want to implement each of these three methods which will now be called on it. This is where you do whatever it is you want to do with the touches.

When you are implementing those you will want to make sure that the touches you are processing are actually in your sub view. You can do that by including the following check in each of the touch methods:

if (CGRectContainsPoint([[self yourSubView] frame], [[touches anyObject] locationInView:[[self yourScrollView] view]])) {
  /* your touch handling code goes here */
}

There is one more thing we need to deal with if you don’t want the scroll view to be able to scroll while you touch your sub view. That is what the subViewRect property is for. In your controller you need to set the subViewRect to the frame of your sub view:

[[[self yourScrollView] view] setSubViewRect:[[self yourSubView] frame]];

Then in the CustomScrollView class you add this method:

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
  UIView* result = [super hitTest:point withEvent:event];
  
  if (CGRectContainsPoint([self subViewRect], point)) {
    [self setScrollEnabled:NO];
  } else {
    [self setScrollEnabled:YES];
  }
  
  return result;
}

Now every time the scroll view receives a touch we check if the touch is in the sub view that we want to capture the touches. If it is then we disable the scroll view’s ability to scroll. If the touch is not in the sub view then we enable the scroll view’s ability to scroll.

And that should be that. Working with views inside of scroll views can sometimes be frustrating but hopefully this gives you a bit of a leg up the next time you need do it.

Related posts

Leave a Comment