I custom a lightGray view. The lightGray view has a property button. The button's backgroundColor is red.
I overwrite the hitTest method in lightGray view.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
if([self pointInside:point withEvent:event]){
return self.testButton;
}
return nil;
}
As a result, the cursor at the point in the image above. I click the button could response.
If I click in the left area of lightGray view. The red button couldn't response.
What's the reason?
My real problem is the same problem in the tableview section headview. I write this demo to test and ask this question.
I was able to reproduce this behaviour even not using a table view, just a custom view, and it really looks strange - if we return button from hitTest, button's action gets fired not at whole area of the gray view, but rather only at some distance around the button.
I added a test view with a tap gesture recognizer looking for difference with button behaviour and it works as expected. So as a workaround you may replace the button with a view plus gesture recognizer.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
// if (CGRectContainsPoint(self.bounds, point)) {
// return self.testButton; // fires only at some distance
// }
if (CGRectContainsPoint(self.bounds, point)) {
return self.testView; // fires for every point inside gray view
}
return [super hitTest:point withEvent:event];
}
- (IBAction)testButtonTouchUpInside:(UIButton *)sender
{
NSLog(#"BTN HIT"); // Not OK
}
- (IBAction)testViewTap:(UITapGestureRecognizer *)sender
{
NSLog(#"VIEW TAP"); // OK
}
iOS7 & iOS8
I need to disable 2 or three fingers scrolling in UIScrollview.
I tried :
[self.scrollView.panGestureRecognizer setMaximumNumberOfTouches:1];
[self.scrollView.panGestureRecognizer setMinimumNumberOfTouches:1];
But it has no effect. It is still possible to scroll with 2 fingers.
If i tried to set max and min to 2. One finger scrolling was disabled but 3 fingers scrolling possible :(
I tried this too, but without success:
for (UIGestureRecognizer* pan in self.scrollView.gestureRecognizers) {
OTTNSLog(#"touches: %ld", (unsigned long)pan.numberOfTouches);
if ([pan isKindOfClass:[UIPanGestureRecognizer class]])
{
UIPanGestureRecognizer *mpanGR = (UIPanGestureRecognizer *) pan;
mpanGR.minimumNumberOfTouches = 1;
mpanGR.maximumNumberOfTouches = 1;
}
if ([pan isKindOfClass:[UISwipeGestureRecognizer class]])
{
UISwipeGestureRecognizer *mswipeGR = (UISwipeGestureRecognizer *) pan;
mswipeGR.numberOfTouchesRequired = 1;
}
}
Does anybody know, how it solve this ?
Thanks.
PROBLEM:
When the UIPanGestureRecognizer is underlying a UIScrollView - which unfortunately does also effect UIPageViewController - the maximumNumberOfTouches is not behaving as expected, the minimumNumberOfTouches however always limits the lower end correctly.
When monitoring these parameters they report back correct values - they seem to do their job - it's just that UIScrollView itself doesn't honor them and ignores their settings!
SOLUTION:
Set the minimumNumberOfTouches to the desired value e.g. 1 and - very importantly - the maximumNumberOfTouches to 2 !!!
myScrollView.panGestureRecognizer.minimumNumberOfTouches = 1;
myScrollView.panGestureRecognizer.maximumNumberOfTouches = 2;
Conform to the UIGestureRecognizerDelegate protocol in your scrollView's #interface declaration. You don't have to set the panGestureRecognizer.delegate for a UIScrollView!!! The delegate is already set because UIScrollView requires to be the delegate of its own pan/pinchGestureRecognizer.
Then implement the UIGestureRecognizer delegate method:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
NSLog(#"%d", gestureRecognizer.numberOfTouches);
NSLog(#"%#", gestureRecognizer.description);
if (gestureRecognizer == self.panGestureRecognizer) {
if (gestureRecognizer.numberOfTouches > 1) {
return NO;
} else {
return YES;
}
} else {
return YES;
}
}
}
AN EVEN SAFER VERSION:
If you have a custom scrollView class and wanna be on the VERY safe side you can also add one more line of code to disambiguate against other scrollViews:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
NSLog(#"%d", gestureRecognizer.numberOfTouches);
NSLog(#"%#", gestureRecognizer.description);
if ([gestureRecognizer.view isMemberOfClass:[MY_CustomcrollView class]]) {
if (gestureRecognizer == self.panGestureRecognizer) {
if (gestureRecognizer.numberOfTouches > 1) {
return NO;
} else {
return YES;
}
} else {
return YES;
}
} else {
return YES;
}
}
ADDITIONAL INFORMATION:
The NSLogs tell you the number of touches. If you set both the min and max to the same value (like 1 in the example above) the if-loop would never be triggered... ;-)
That is why maximumNumberOfTouches has to be at least minimumNumberOfTouches + 1
panGestureRecognizer.minimumNumberOfTouches = 1;
panGestureRecognizer.maximumNumberOfTouches = minimumNumberOfTouches + 1;
GEEK SECTION:
for (UIView *view in self.view.subviews) {
if ([view isKindOfClass:[UIScrollView class]]) {
NSLog(#"myPageViewController - SCROLLVIEW GESTURE RECOGNIZERS: %#", view.gestureRecognizers.description);
((UIPanGestureRecognizer *)view.gestureRecognizers[1]).minimumNumberOfTouches = 1;
((UIPanGestureRecognizer *)view.gestureRecognizers[1]).maximumNumberOfTouches = 2;
}
}
This is the way to access the underlying scrollView that is responsible for the paging of a UIPageViewController. Put this code in the e.g. viewDidLoad: of the UIPageViewController (self).
If you don't have access to the scrollView at all - like for a UIPageViewController in a dynamic UITableViewCell where creation and cell reuse happens at runtime and no outlets can be set on its contentViews - put a category on UIScrollView and override the delegate method there. But be careful! This effects every scrollView in your application - so do proper introspection (class-checking) like in my 'EVEN SAFER' example above... ;-)
SIDENOTE:
Unfortunately the same trick doesn't work with the pinchGestureRecognizer on UIScrollView because it doesn't expose a min/maxNumberOfTouches property. When monitored it always reports 2 touches (which you obviously need to pinch) - so its internal min/maxNumberOfTouches seem to have been set both to 2 - even if UIScrollView isn't honoring its own settings and keeps happily pinching with any numbers of fingers (more than 2). So there is no way to restrict pinching to a limited amount of fingers...
I can confirm this is still an issue in iOS 8, but only when the UIPanGestureRecognizer is underlying a UIScrollView. Creating a UIView with a fresh UIPanGestureRecognizer and setting its maximumNumberOfTouches property works as expected.
Interesting note: if you query the UIScrollView's UIPanGestureRecognizer while you're scrolling, it reports the number of touches as less than or equal to the maximum. In other words, if you set the maximum to 2, and scroll with 3 fingers, it reports the gesture as a 2-finger scroll. Subsequently letting up fingers one at a time while continuing to scroll usually (but not consistently) reduces the reported number of touches as well — so if you go from 3 -> 2 -> 1 fingers, it will register 2 -> 1 -> 0 touches, and stop scrolling while you still have 1 finger on the device.
Submitted rdar://20890684 and copied to http://openradar.appspot.com/radar?id=6191825677189120. Please feel free to dupe.
There is no specific method available for this just do some tricks but results are only 75%.
Add swipe gestures(4 directions each) and double tap gesture to your UIScrollview..Then use below code..
[[yourScrollView panGestureRecognizer] requireGestureRecognizerToFail:swipeDown];
[[yourScrollView panGestureRecognizer] requireGestureRecognizerToFail:swipeLeft];
[[yourScrollView panGestureRecognizer] requireGestureRecognizerToFail:swipeRight];
[[yourScrollView panGestureRecognizer] requireGestureRecognizerToFail:swipeUp];
[[yourScrollView panGestureRecognizer] requireGestureRecognizerToFail:doubleTap];
This will do the job for you:
yourscrollView.multipleTouchEnabled=NO;
I found some questions and answers here on stackoverflow for that problem, but none of the solutions there solved my problem.
My iOS App has the ability to play some music with a nice music player. I designed it with Xcode's Interface Builder and dragged out a UIView and changed its class to MPVolumeView. Everything works fine when I'm debugging my app on my iPhone 6.
Here is my problem: I also dragged out a UITapGestureRecognizer on my whole view which contains my controls like
play/pause, next/previous track (...)
and also my MPVolumeView. When I tap on that view it should fade out and disappear. Then I added a UITapGestureRecognizer on my UIImageView which shows my artwork image of the song. When I tap this image view, it should fade in my view with all controls in int - that's working properly.
BUT: When I slide the knob of the volume slider just a little bit, or if I am just touching it, the view still disappears. It seems like my MPVolumeView is forwarding my touch or something like that. I tried setting userInteractionEnabled = false on my volume slider, but that didn't help. I also set the delegate of my gesture recognizer to self and added the
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
NSLog(#"tapped");
if([gestureRecognizer.view isMemberOfClass:[UIImageView class]]) {
return true;
}
return false;
}
function to my code, which returns true or false, depending on which view I'm tapping. When I'm accessing the gestureRecognizer.view property, it doesn't recognize my MPVolumeView, just the UIView in the background.
Here my two methods which are fired after when the TapGestureRecognizers are fired:
- (IBAction)overlayViewTapped:(UITapGestureRecognizer *)sender {
if(sender.state == UIGestureRecognizerStateEnded) {
[UIView animateWithDuration:0.3
delay:0.0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{ self.blackOverlayView.alpha = 0.0; self.normalTimeLabel.alpha = 1.0; }
completion:nil];
}
}
- (IBAction)imageViewTapped:(UITapGestureRecognizer *)sender {
[UIView animateWithDuration:0.3
delay:0.0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{ self.blackOverlayView.alpha = 1.0; self.normalTimeLabel.alpha = 0.0; }
completion:nil];
}
Please help me, I'm nearly going nuts with that ..
EDIT: My music player looks like this:
After I tap anywhere on the view (except the subviews), the view should fade out and hide everything, just show the artwork image of the song and the current elapsed time. This will look like this:
As I said - the problem is, if I just tap the volume slider or slide it just a little bit, my UITapGestureRecognizer fires and fades out my complete view. How can I prevent that?
It is behaving the way it is simply because you added the gesture recognizer to the entire UIView, which includes the volume slider and whatnot.
Instead of detecting the touch in the entire view, check to see if the touch is in the area you want it.
Create a CGRect property, I'll call it touchArea:
#property CGRect touchArea;
Then specify the size of the touchArea (you can do this in the viewDidLoad):
touchArea = CGRectMake(0.0, 240.0, 320.0, 240.0);
You will have to find out where you want this and how big it should be and replace my example values with the real ones. A simple way of cheating this is to take something like a UILabel in IB and positioning and sizing it to your desire, then go to the size inspector pane and get the x, y, width and height values.
Then, before you do your fade animation, check to see if the touch was in the touchArea:
- (void)handleGesture:(UIGestureRecognizer *)gestureRecognizer
{
CGPoint touchPoint = [gestureRecognizer locationInView:self.view];
if (CGRectContainsPoint(touchArea, touchPoint))
{
//do your animation here.
}
}
As a note, I would set a BOOL to check whether or not the view is faded in or out, so you can always check before animating.
While I was playing on my phone, I noticed that my UISegmentedControl was not very responsive. It would take 2 or more tries to make my taps register. So I decided to run my app in Simulator to more precisely probe what was wrong. By clicking dozens of times with my mouse, I determined that the top 25% of the UISegmentedControl does not respond (the portion is highlighted in red with Photoshop in the screenshot below). I am not aware of any invisible UIView that could be blocking it. Do you know how to make the entire control tappable?
self.segmentedControl = [[UISegmentedControl alloc] initWithItems:[NSArray arrayWithObjects:#"Uno", #"Dos", nil]];
self.segmentedControl.selectedSegmentIndex = 0;
[self.segmentedControl addTarget:self action:#selector(segmentedControlChanged:) forControlEvents:UIControlEventValueChanged];
self.segmentedControl.height = 32.0;
self.segmentedControl.width = 310.0;
self.segmentedControl.segmentedControlStyle = UISegmentedControlStyleBar;
self.segmentedControl.tintColor = [UIColor colorWithWhite:0.9 alpha:1.0];
self.segmentedControl.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
UIView* toolbar = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.width, HEADER_HEIGHT)];
toolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth;
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = CGRectMake(
toolbar.bounds.origin.x,
toolbar.bounds.origin.y,
// * 2 for enough slack when iPad rotates
toolbar.bounds.size.width * 2,
toolbar.bounds.size.height
);
gradient.colors = [NSArray arrayWithObjects:
(id)[[UIColor whiteColor] CGColor],
(id)[[UIColor
colorWithWhite:0.8
alpha:1.0
] CGColor
],
nil
];
[toolbar.layer insertSublayer:gradient atIndex:0];
toolbar.backgroundColor = [UIColor navigationBarShadowColor];
[toolbar addSubview:self.segmentedControl];
UIView* border = [[UIView alloc] initWithFrame:CGRectMake(0, HEADER_HEIGHT - 1, toolbar.width, 1)];
border.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
border.backgroundColor = [UIColor colorWithWhite:0.7 alpha:1.0];
border.autoresizingMask = UIViewAutoresizingFlexibleWidth;
[toolbar addSubview:border];
[self.segmentedControl centerInParent];
self.tableView.tableHeaderView = toolbar;
http://scs.veetle.com/soget/session-thumbnails/5363e222d2e10/86a8dd984fcaddee339dd881544ecac7/5363e222d2e10_86a8dd984fcaddee339dd881544ecac7_20140509171623_536d6fd78f503_68_896x672.jpg
As already written in other answers, UINavigationBar grabs the touches made near the nav bar itself, but not because it has some subviews extended over the edges: this is not the reason.
If you log the whole view hierarchy, you will see that the UINavigationBar doesn't extends over the defined edges.
The reason why it receives the touches is another:
in UIKit, there are many "special cases", and this is one of them.
When you tap the screen, a process called "hit testing" starts. Starting from the first UIWindow, all views are asked to answer two "questions": is the point tapped inside your bounds? what is the subviews that must receive the touch event?
this questions are answered by these two methods:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
Ok, now we can continue.
After the tap, UIApplicationMain starts the hit testing process. The hit test starts from the main UIWindow (and is executed even on the status bar window and the alert view window, for example), and goes through all subviews.
This process is executed 3 times:
two times starting from UIWindow
one times starting from _UIApplicationHandleEvent
If you tap on the Navigation Bar, you will see that hitTest on UIWindow will return the UINavigationBar (all three times)
If you tap on the area below the Navigation Bar however, you will se something strange:
the first two hitTest will return your UISegmentedControl
the last hitTest will return UINavigationBar
why this?
If you swizzle and subclass UIView, overriding hitTest, you will see that the first two times the tapped point is correct. The third time, something changes the point doing something like point - 15 (or a similar number)
After a lot of searching, I have found where this is happening:
UIWindow has a (private) method called
-(CGPoint)warpPoint:(CGPoint)point;
debugging it, I saw that this method changes the tapped point if it is immediately below the status bar.
Debugging more, I saw that the stack calls that make this possible, are only 3:
[UINavigationBar, _isChargeEnabled]
[UINavigationBar, isEnabled]
[UINavigationBar, _isAlphaHittableAndHasAlphaHittableAncestors]
So, at the end, this warpPoint method checks if the UINavigationBar is enabled and hittable, if yes it "warps" the point. The point is warped of a number of pixel between 0 and 15, and this "warp" increases when you get closer to the Navigation Bar.
Now that you know what happens behind the scenes, you have to know how to avoid it (if you want).
You can't simply override warpPoint: if the application must go on the AppStore: it's a private method and your app will be rejected.
You have to find another system (like as suggested, overriding sendEvent, but I'm not sure if it will work)
Because this question is interesting, I will think about a legal solution tomorrow and update this answer (one good starting point can be subclassing UINavigationBar, overriding hitTest and pointInside, returning nil/false if, given the same event over multiple calls, the point changes. But I must test if it works tomorrow)
EDIT
Ok, I've tried many solutions but it's not simple to find a legal and stable one.
I've described the actual behavior of the system, that could vary on different versions (hitTest called more or less than 3 times, the warpPoint warping the point of about 15px that can change ecc ecc).
The most stable is obviously the illegal override of warpPoint: in a UIWindow subclass:
-(CGPoint)warpPoint:(CGPoint)point;
{
return point;
}
however, I've found that a method like this (in UIWindow subclass) it's stable enough and does the trick:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// this method is not safe if you tap the screen two times at the same x position and y position different for 16px, because it moves the point
if (self.lastPoint.x == point.x)
{
// the points are on the same vertical line
if ((0 < (self.lastPoint.y - point.y)) && ((self.lastPoint.y - point.y) < 16) )
{
// there is a differenc of ~15px in the y position?
// if so, the point has been changed
point.y = self.lastPoint.y;
}
}
self.lastPoint = point;
return [super hitTest:point withEvent:event];
}
This method records the last point tapped, and if the subsequent tap is at the same x, and an y different for max 16px, then uses the previous point.
I've tested a lot and it seems stable.
If you want, you can add more controls to enable this behavior only in particular controllers, or only on a defined portion of the window, ecc ecc.
If I find another solution, I'll update the post
I believe the problem is because the buttons in the UINavigationBar have a larger than normal touch area. See this SO post. You can also find plenty of discussion on this with a 'UINavigationBar touch area' Google search.
As a possible solution, you could put the segmented control IN the navigation bar, but you would know better than I if that fits your use cases or not.
I've come up with an alternate solution that to me seems safer than LombaX's. It uses the fact that both events come in with the same timestamp to reject the subsequent event.
#interface RFNavigationBar ()
#property (nonatomic, assign) NSTimeInterval lastOutOfBoundsEventTimestamp;
#end
#implementation RFNavigationBar
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// [rfillion 2014-03-28]
// UIApplication/UIWindow/UINavigationBar conspire against us. There's a band under the UINavigationBar for which the bar will return
// subviews instead of nil (to make those tap targets larger, one would assume). We don't want that. To do this, it seems to end up
// calling -hitTest twice. Once with a value out of bounds which is easy to check for. But then it calls it again with an altered point
// value that is actually within bounds. The UIEvent it passes to both seem to be the same. However, we can't just compare UIEvent pointers
// because it looks like these get reused and you end up rejecting valid touches if you just keep around the last bad touch UIEvent. So
// instead we keep around the timestamp of the last bad event, and try to avoid processing any events whose timestamp isn't larger.
if (point.y > self.bounds.size.height)
{
self.lastOutOfBoundsEventTimestamp = event.timestamp;
return nil;
}
if (event.timestamp <= self.lastOutOfBoundsEventTimestamp + 0.001)
{
return nil;
}
return [super hitTest:point withEvent:event];
}
#end
You might want to check which view is recording the touches. Try this method-
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
[touch locationInView:self.view];
if([touch.view isKindOfClass:[UISegmentedControl class]])
{
NSLog(#"This is UISegment");
}
else if([touch.view isKindOfClass:[UITabBar class]])
{
NSLog(#"This is UITabBar");
} else if(...other views...) {
...
}
}
Once you figure that out you maybe able to narrow down your problem.
It looks as if you're using a category extension to set width/height on views, as well as center them in their parent. Perhaps there is a hidden issue here - can you refactor to do your layout w/out this category?
I copied your code into a clean project and ran it in a UITableViewController's viewDidLoad method - it works fine and I have no dead spots like you report. I had to change your code slightly since I don't have the same category extension that you're using.
Also, if you're running this code in viewDidLoad, you should verify that your view has a defined size (you access your view.width). If you're creating your UITableViewController programmatically (vs from a nib/storyboard) then the frame may be CGRectZero. Mine was loaded from a nib so the frame was preset.
I'd also try temporarily removing your border view to see if it's the culprit.
I recommend that you avoid having touch-sensitive UI in such close proximity to the nav bar or toolbar. These areas are typically known as "slop factors" making it easier for users to perform touch events on buttons without the difficulty of performing precision touches. This is also the case for UIButtons for example.
But if you want to capture the touch event before the navigation bar or toolbar receives it, you can subclass UIWindow and override: -(void)sendEvent:(UIEvent *)event;
An easy way to debug this is to try using DCIntrospect in your project. It's a very easy to use/implement library that makes finding out what views are where when in the simulator a breeze.
Install the library and configure it
Run the application in the simulator and navigate to the screen with the issue
Press spacebar on the keyboard (the computer keyboard, not the simulator's
keyboard)
Click on the 25% area and see what gets highlighted.
If what's highlighted isn't the segmented view controller, that view could be what's covering up the touch event.
Create a protocol for UINavigationBar: (add new file and paste below code)
/******** file: UINavigationBar+BelowSpace.h*******/
"UINavigationBar+BelowSpace.h"
#import <Foundation/Foundation.h>
#interface UINavigationBar (BelowSpace)
#end
/*******- file: UINavigationBar+BelowSpace.m*******/
#import "UINavigationBar+BelowSpace.h"
#implementation UINavigationBar (BelowSpace)
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
int errorMargin = 5;// space left to decrease the click event area
CGRect smallerFrame = CGRectMake(0 , 0 - errorMargin, self.frame.size.width, self.frame.size.height);
BOOL isTouchAllowed = (CGRectContainsPoint(smallerFrame, point) == 1);
if (isTouchAllowed) {
self.userInteractionEnabled = YES;
} else {
self.userInteractionEnabled = NO;
}
return [super hitTest:point withEvent:event];
}
#end
Hope this help ^ ^
Try this
self.navigationController!.navigationBar.userInteractionEnabled = false;
My interface sometimes has buttons around its periphery. Areas without buttons accept gestures.
GestureRecognizers are added to the container view, in viewDidLoad. Here’s how the tapGR is set up:
UITapGestureRecognizer *tapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(playerReceived_Tap:)];
[tapGR setDelegate:self];
[self.view addGestureRecognizer:tapGR];
In order to prevent the gesture recognizers from intercepting button taps, I implemented shouldReceiveTouch to return YES only if the view touched is not a button:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gr
shouldReceiveTouch:(UITouch *)touch {
// Get the topmost view that contains the point where the gesture started.
// (Buttons are topmost, so if they were touched, they will be returned as viewTouched.)
CGPoint pointPressed = [touch locationInView:self.view];
UIView *viewTouched = [self.view hitTest:pointPressed withEvent:nil];
// If that topmost view is a button, the GR should not take this touch.
if ([viewTouched isKindOfClass:[UIButton class]])
return NO;
return YES;
}
This works fine most of the time, but there are a few buttons that are unresponsive. When these buttons are tapped, hitTest returns the container view, not the button, so shouldReceiveTouch returns YES and the gestureRecognizer commandeers the event.
To debug, I ran some tests...
The following tests confirmed that the button was a sub-subview of the container view, that it was enabled, and that both button and the subview were userInteractionEnabled:
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gr
shouldReceiveTouch:(UITouch *)touch {
// Test that hierarchy is as expected: containerView > vTop_land > btnSkipFwd_land.
for (UIView *subview in self.view.subviews) {
if ([subview isEqual:self.playComposer.vTop_land])
printf("\nViewTopLand is a subview."); // this prints
}
for (UIView *subview in self.playComposer.vTop_land.subviews) {
if ([subview isEqual:self.playComposer.btnSkipFwd_land])
printf("\nBtnSkipFwd is a subview."); // this prints
}
// Test that problem button is enabled.
printf(“\nbtnSkipFwd enabled? %d", self.playComposer.btnSkipFwd_land.enabled); // prints 1
// Test that all views in hierarchy are interaction-enabled.
printf("\nvTopLand interactionenabled? %d", self.playComposer.vTop_land.userInteractionEnabled); // prints 1
printf(“\nbtnSkipFwd interactionenabled? %d", self.playComposer.btnSkipFwd_land.userInteractionEnabled); // prints 1
// etc
}
The following test confirms that the point pressed is actually within the button’s frame.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gr
shouldReceiveTouch:(UITouch *)touch {
CGPoint pointPressed = [touch locationInView:self.view];
CGRect rectSkpFwd = self.playComposer.btnSkipFwd_land.frame;
// Get the pointPressed relative to the button's frame.
CGPoint pointRelSkpFwd = CGPointMake(pointPressed.x - rectSkpFwd.origin.x, pointPressed.y - rectSkpFwd.origin.y);
printf("\nIs relative point inside skipfwd? %d.", [self.playComposer.btnSkipFwd_land pointInside:pointRelSkpFwd withEvent:nil]); // prints 1
// etc
}
So why is hitTest returning the container view rather than this button?
SOLUTION: The one thing I wasn't testing was that the intermediate view, vTop_land, was framed properly. It looked OK because it had an image that extended across the screen -- past the bounds of its frame (I didn't know this was possible). The frame was set to portrait width, rather than landscape width, so buttons on the far right were out of zone.
Hit test is not reliable in most cases, and it is generally not advisable to use it along with gestureRecognizers.
Why dont you setExclusiveTouch:YES for each button, and this should make sure that the buttons are always chosen.