Iphone – UIScrollview subviews outside of clipping bounds not receiving touches

ipadiphoneuiscrollview

I have a UIScrollView which I have set up to swipe one column at a time (two columns per page) – by setting the frame to half of the views actual width, setting clipToBounds to NO and use hitTest to declare the area outside of the frame as belonging to the UIScrollView (see example below).

This works great, but my problem now is that the subviews of the UIScrollView don't get any touch events – only the main UIScrollView does.

In the following example, if the hitTest code is included, then the scrollview scrolls correctly, paging one column at a time and all its content may be seen – but the inner scrollviews do not receive touch events.

If I remove the hitTest code, then only the first child scrollview receives touches, and all its content may be seen – but the main scrollview wont get touches in the non-clipped area.

How can I solve this?

Example:

//=========================================
// UIScrollViewEx
// Just in order to log touches...
//=========================================

@interface UIScrollViewEx : UIScrollView {} 
@end

@implementation UIScrollViewEx
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touches Began (0x%08X)", (unsigned int)self);
}
@end

//=========================================
// UIViewEx
// Dummy class - sets subview as hit target
// just to demonstrate usage of non-clipped 
// content
//=========================================

@interface UIViewEx : UIView {} 
@end

@implementation UIViewEx
- (UIView *) hitTest:(CGPoint) point withEvent:(UIEvent *)event {
    if ([self pointInside:point withEvent:event]) {
        return [self.subviews objectAtIndex:0];
    }
    return nil;
}
@end

//=========================================
// MainClass
// Any UIViewEx based class which returns
// the UIScrollView child on hittest
//=========================================

@implementation MyClass

- (UIColor*) randomColor
{
    int r = arc4random() % 100;
    int g = arc4random() % 100;
    int b = arc4random() % 100;
    return [UIColor colorWithRed:(0.01 * r) green:(0.01 * g) blue:(0.01 * b) alpha:1.0];
}

- (void) loadScrollviews
{
    // Set frame to half of actual width so that paging will swipe half a page only
    CGRect frame = CGRectMake(0, 0, self.bounds.size.width / 2, 400);

    // Main scrollview
    UIScrollView *scrollview = [[UIScrollView alloc] initWithFrame:frame];
    [scrollview setBackgroundColor:[UIColor greenColor]];
    [scrollview setPagingEnabled:YES];
    [scrollview setClipsToBounds:NO];

    // Create smaller scrollviews inside it - each one half a screen wide
    const int numItems = 5;
    for(int i = 0; i < numItems; ++i) {
        frame.origin.x = frame.size.width * i;
        UIScrollView *innerScrollview = [[UIScrollViewEx alloc] initWithFrame:frame];
        [innerScrollview setContentSize:CGSizeMake(frame.size.width, 1000)];
        [innerScrollview setBackgroundColor:[self randomColor]];
        [scrollview addSubview:innerScrollview];
        [innerScrollview release];
    }

    [scrollview setContentSize:CGSizeMake(numItems * frame.size.width, frame.size.height)];

    [self addSubview:scrollview];
}

@end

Update
I get the touches forwarded to the inner view by doing the following, but surely there must be a better way?

- (UIView *) hitTest: (CGPoint) pt withEvent: (UIEvent *) event 
{    
    if(CGRectContainsPoint(self.bounds, pt)) 
    {
        UIScrollView *scrollview = [self.subviews objectAtIndex:0];
        CGPoint scrollViewpoint = [scrollview convertPoint:pt fromView:self];

        for(UIView *view in scrollview.subviews) {
            if(CGRectContainsPoint(view.frame, scrollViewpoint)) {
                return view;
            }
        }
        return scrollview;
    } 
    else {
        return [super hitTest:pt withEvent:event];
    }
}

Best Answer

This may work:

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {
    UIView* child = nil;
    if ((child = [super hitTest:point withEvent:event]) == self)
        return self.scrollView;         
    return child;
}

But if subview is out of scrollview bounds events were not fired and this function returns self.scrollView.

Related Topic