I have a view which I'll call parentView which has a subview called childView. Part of childView is outside the bounds of parentView, and childView has a panGestureRecognizer attached to it. I have implemented the following in parentView so that it will recognize touches to childView even though it's outside of its superviews bounds:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (!self.clipsToBounds && !self.hidden && self.alpha > 0)
{
for (UIView *subview in self.subviews)
{
CGPoint subPoint = [subview convertPoint:point fromView:self];
UIView *result = [subview hitTest:subPoint withEvent:event];
if (result != nil)
{
return result;
break;
}
}
}
return [super hitTest:point withEvent:event];
}
Yet when I touch or drag childView, hitTest is not even being called on the parentView. Why is this?
because the event goes down the responder chain and is used before hittest gets called
so the event goes from top to bottom in this case... check out the documentation concerning the responder chain:
it is not very clear though :D
http://developer.apple.com/library/ios/#DOCUMENTATION/EventHandling/Conceptual/EventHandlingiPhoneOS/Introduction/Introduction.html
BUT the important bit:
The hit-test view is given the first opportunity to handle a touch event. If the hit-test view cannot handle an event, the event travels up that view’s chain of responders as described in “The Responder Chain Is Made Up of Responder Objects” until the system finds an object that can handle it.
Touch events. If the hit-test view cannot handle a touch event, the event is passed up a chain of responders that starts with the hit-test view.
Related
How should I configurative my UIScrollView such that, a UIbutton's forControlEvents:UIControlStateHighlighted can still be triggered when the scrollView is in the state of scrolling.
Now it simply stops the scrolling when touched, instead of highlighting the button even though the finger landed on it.
This is very expected, of course, but I would really appreaciate if someone can guide me to enabling button's touch event when scrolling.
Well, you could try to subclass UIScrollView and override the hitTest method like this:
-(id)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
id hitView = [super hitTest:point withEvent:event];
if (hitView == yourButton) {
return yourButton;
} else {
return hitView;
}
}
That way, when your button is being "hit", the button would receive the touch event instead of the UIScrollView.
I have a bunch of UIViews like in the image below. The red/pink (semi-transparent) view is on top of the others.
Red has a UISwipeGestureRecognizer.
Green has as a UITapGestureRecognizer.
Blue has no recognizer.
A tap on the visible (bottom-left) part of Green trigger its recognizer.
A tap on the hidden parts of Green does not trigger its recognizer (Red blocks it).
That's the problem: I want Green to trigger. How can I do this?
In practice, the views may be in any order, any number and be subviews of each others etc. But the problem is the same:
How can I reliably find the uppermost view that can handle the gesture (tap or swipe)?
I tried with the code below. It neatly traverses all views, but it fails since it cannot know if the event is part of a swipe or a tap. So the method always returns the red view. If I remove the swipe-recognizer from Red, the code works correctly.
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *hitView = [super hitTest:point withEvent:event];
if (hitView == self)
{
if (self.hasASwipeRecognizer)
return self; // What if this was a tap?
if (self.hasATapRecognizer)
return self;
else
return nil;
}
else
return hitView;
}
An alternative to adding the gesture recognizer to these views would be to add the gesture recognizers to the parent view and handle the use cases appropriately using the delegate method gestureRecognizer:shouldReceiveTouch: method.
Identify whether the particular recognizer should receive the touch and return YES. For example, if the gesture recognizer passed is a swipe recognizer then check if the touch point is within the green view and return YES. Return NO otherwise.
If there are similar gesture recognizers then I suggest that you keep a reference and verify against it.
Usage
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
CGPoint pointInView = [touch locationInView:gestureRecognizer.view];
if ( [gestureRecognizer isMemberOfClass:[UITapGestureRecognizer class]]
&& CGRectContainsPoint(self.blueView.frame, pointInView) ) {
return YES;
}
if ( [gestureRecognizer isMemberOfClass:[UISwipeGestureRecognizer class]]
&& CGRectContainsPoint(self.greenView.frame, pointInView) ) {
return YES;
}
return NO;
}
One possible solution would be to add a tap gesture recognizer to the top red view and then whenever you get the tap, check whether the tap point intersects with the green view. If so, forward the tap to that view. If not, ignore the tap.
My solutions is:
-(void)handleGesture:(UIGestureRecognizer*)gestureRecognizer {
CGPoint touchPoint = [tapGestureRecognizer locationInView:viewUnderTest];
if ([viewUnderTest pointInside:touchPoint withEvent:nil]) {
NSLog(#"Hit done in view under test");
}
}
Basically, here is my view hierarchy (and I appologize if this is hard to read... I'm new here so posting suggestions happily accepted)
--AppControls.xib
-------(UIView)ControlsView
----------------- (UIView)TopBar
----------------- -------------- btn1, btn2, btn3
----------------- UIView)BottomBar
----------------- --------------slider1 btn1, btn2
--PageContent.xib
----------------- (UIView)ContentView
----------------- --------------btn1, btn2, btn3
----------------- --------------(UIImageView)FullPageImage
My situation is that I want to hide and show the controls when tapping anywhere on the PageContent thats not a button and have the controls show, much like the iPhone Video Player. However, when the controls are shown I still want to be able to click the buttons on the PageContent.
I have all of this working, except for the last bit. When the controls are showing the background of the controls receives the touch events instead of the view below. And turning off user interaction on the ControlsView turns it off on all its children.
I have tried overriding HitTest on my ControlsView subclass as follows which I found in a similar post:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *hitView = nil;
NSArray *subviews = [self subviews];
int subviewCount = [subviews count];
for (int subviewIndex = 0; !hitView && subviewIndex < subviewCount; subviewIndex++){
hitView = [[subviews objectAtIndex:subviewIndex] hitTest:point withEvent:event];
}
return hitView;
}
However, at this point my slider doesn't work, nor do most of the other buttons, and really, things just start getting weird.
So my question is in short: How do I let all the subviews of a view have touch events, while the super view's background is unclickable, and the buttons on views below can receive touch events.
Thanks!
You're close. Don't override -hitTest:withEvent:. By the time that is called, the event dispatcher has already decided that your subtree of the hierarchy owns the event and won't look elsewhere. Instead, override -pointInside:withEvent:, which is called earlier in the event processing pipeline. It's how the system asks "hey view, does ANYONE in your hierarchy respond to an event at this point?". If you say NO, event processing continues below you in the visible stack.
Per the documentation, the default implementation just checks whether the point is in the bounds of the view at all.
Your strategy is to say "yes" when any of your subviews is at that coordinate, but say "no" when the touch would be hitting the background.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
for (UIView * view in [self subviews]) {
if (view.userInteractionEnabled && [view pointInside:[self convertPoint:point toView:view] withEvent:event]) {
return YES;
}
}
return NO;
}
Thanks to #Ben Zutto, Swift 3 solution:
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
for view in self.subviews {
if view.isUserInteractionEnabled, view.point(inside: self.convert(point, to: view), with: event) {
return true
}
}
return false
}
Another approach may be to have an invisible full-screen button behind everything else, and take appropriate action when it is hit.
A slight variant on Ben's answer, dealing w/ children which extend outside their parent.
If clipChildren is YES, then this will not return YES for points which are outside the main control but inside some child.
if clipChildren is NO, this is the same as Ben's.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
BOOL clipChildren = YES;
if (!clipChildren || [super pointInside:point withEvent:event]) {
for (UIView * view in [self subviews]) {
if (view.userInteractionEnabled && [view pointInside:[self convertPoint:point toView:view] withEvent:event]) {
return YES;
}
}
}
return NO;
}
I have a view within a UIScrollView that loads an additional subview when the user presses a certain area. When this additional subview is visible, I want all touch events to be handled by this - and not by the scrollview.
It seems like the first couple events are being handled by the subview, but then touchesCancelled is called and the scrollview takes over the touch detection.
How can I make sure that the subview gets all the events as long as the movement activity is being performed on this view?
This is my implementation on touchesMoved - which I thought would do the job...
-(void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [[touches allObjects] objectAtIndex:0];
CGPoint touchPt = [touch locationInView:self];
UIView *hitView = [self hitTest:touchPt withEvent:event];
UIView *mySubView = subviewCtrl.view;
if(hitView == mySubView) {
[subviewCtrl.view touchesMoved:touches withEvent:event];
}
else {
NSLog(#"Outside of view...");
}
}
The responder chain hierarchy "normally" goes from subview to superview, so you shouldn't need to do the hitTest in your superview. The problem that you are having is not that you need the superview to invoke touchesMoved on the subview, but rather that UIScrollView subverts the normal responder chain hierarchy by intercepting touch events in order to deliver a smooth scrolling experience to the user. If you don't want this behaviour, then you can disable this behaviour in the scrollView by sending it the following message:
[scrollView setDelaysContentTouches:NO];
Note that this will make sure that your subview has first crack at handling the events in question (provided that it is in fact the first responder). This can negatively impact the scrolling and zooming performance of the scrollView, however, which is probably why Apple sets it to YES by default.
I have a view derived from a UIScrollView controller that I use to page through images in a library. That works fine.
I overlayed a UIView on the right side to handle touches for scrolling quickly through the list of images. My problem is that if I pass the touches through to either 'super' or 'nextResponder' they never make it to the UIScrollView object below.
My question is how can I force the UIScrollView below to handle the touches that I don't need to handle? I'm setting a timer for 0.3 seconds that during that time all touches are passed to the UIScrollView to handle. So if the user started a swipe gesture to turn the page, it will happen.
Here's the code for the touchesBegan method:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
// Start a timer to trigger the display of the slider and the processing of events.
touchesTimer = [NSTimer scheduledTimerWithTimeInterval:0.3 target:self selector:#selector(showSlider:) userInfo:nil repeats:NO];
// Where are we at right now?
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self.view];
lastPage = [self page:currentPoint.y];
// Pass the event though until we need it.
// [self.nextResponder touchesBegan:touches withEvent:event];
if ([touch.view isKindOfClass:[UIScrollView class]])
{
if (self.nextResponder != nil &&
[self.nextResponder respondsToSelector:#selector(touchesEnded:withEvent:)])
{
[self.nextResponder touchesEnded:touches withEvent:event];
}
}
}
I would try using a regular UIViewController rather than a UIScrollViewController and then add the scroll view object where you need it and put the scroll faster view you want on the right side. This way they are separate and the touches won't get confused.
Here's an easier solutions that worked well for me:
In the UIView on top of the UIScrollview make the width and height both 0 (so that the view is no longer technically on top of the UIScrollView) and set clipsToBounds = NO (so that the contents of the view still show up on top of the UIScrollView).
self.OverlayView.clipsToBounds = NO;
CGRect frame = self.OverlayView.frame;
self.OverlayView.frame = CGRectMake(frame.origin.x, frame.origin.y, 0, 0);
Note that if the UIView contains interactive controls then they will no longer work. You'll need to move it into it's own view above the UIScrollView.