I have a UIScrollView with 2 pages, and I can scroll horizontally between them. However, on one of my pages, I have a UIDatePicker, and the scroll view is intercepting the vertical touch events so I can no longer manipulate the date picker (except by clicking or tapping). Is there some way to tell the ScrollView to send the vertical touch events to the date picker, but send the horizontal touch events to the scroll view to switch pages?
I think there's two parts to this problem. The first is determining the user's intent, and the second is getting the correct control to respond to that intent.
Determining Intent
I think it's important to be clear about what the user intends. Imagine this scenario: The user starts touching the screen and moves his finger far to the left, but also up a little. The user probably intended to scroll the view, and didn't intend to change the date at all. It would be bad to both scroll the view and change the date, especially just as it moves off-screen. So to determine what the user intends I suggest the following algorithm:
When the user starts touching the screen, record the starting position. As the user's finger starts to move away from that position, the controls should not react at all. Once the touch moves past a certain threshold distance from the starting position, determine whether it moved more horizontally or vertically. If it moved vertically, the user intends to change the date, so ignore the horizontal portion of the movement and only change the date. If it moved more horizontally, the user intends to scroll the view, so ignore the vertical portion of the movement and only scroll the view.
Implementation
In order to implement this, you need to handle the events before the UIScrollView or date picker do. There's probably a few ways to do this, but one in particular comes to mind: Make a custom UIView called ScrollingDateMediatorView. Set the UIScrollView as a child of this view. Override the ScrollingDateMediatorView's hitTest:withEvent: and pointInside:withEvent: methods. These methods need to perform the same kind of hit testing that would normally occur, but if the result is the date picker, return self instead. This effectively hijacks any touch events that were destined for the date picker, allowing the ScrollingDateMediatorView to handle them first. Then you implement the algorithm described above in the various touches* methods. Specifically:
In the touchesBegan:withEvent method, save the starting position.
In touchesMoved:withEvent, if the user's intent isn't known yet, determine whether the touched has moved far enough away from the starting position. If it has, determine whether the user intends to scroll or change the date, and save that intent. If the user's intent is already known and it's to change the date, send the date picker the touchedMoved:withEvent message, otherwise send the UIScrollView the touchesMoved:withEvent message. You'll have to do some simliar work within touchesEnded:withEvent and touchesCancelled:withEvent to make sure the other views get the appropriate messages. Both of these methods should reset the saved values.
Once you have it properly propagating events, you'll probably have to try some user testing to tune the movement threshold.
Actually, there is a much simpler implementation than what Bob suggested. This works perfectly for me. You will need to subclass your UIScrollview if you haven't already, and include this method:-
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
if ([result.superview isKindOfClass:[UIPickerView class]])
{
self.canCancelContentTouches = NO;
self.delaysContentTouches = NO;
}
else
{
self.canCancelContentTouches = YES; // (or restore bool from prev value if needed)
self.delaysContentTouches = YES; // (same as above)
}
return result;
}
The reason I use result.superview
is that the view which gets the touches will actually be a UIPickerTable, which is a private API.
Cheers