I'm trying to track hits on UI elements (tap and long press) using UIGestureRecognizer. After hit was tracked (let's say logged via NSLog) UI element should do it's job.
I'm creating gesture recognizers like this:
UITapGestureRecognizer* tap = [[UITapGestureRecognizer] alloc initWithTarget:self action:(OnGesture:)]
tap.cancelsTouchesInView = NO;
tap.delegate = self;
[view addGestureRecognizer:tap];
UILongPressGestureRecognizer* longPress = [[UILongPressGestureRecognizer] alloc initWithTarget:self action:(OnGesture:)]
longPress.cancelsTouchesInView = NO;
longPress.delegate = self;
[view addGestureRecognizer:longPress];
I've overridden some gesture recognizer methods:
-(BOOL)gestureRecognizer:(UIGestureRecognizer*)_recognizer shouldReceiveTouch(UITouch*)_touch
{
return YES;
}
-(BOOL)gestureRecognizer:(UIGestureRecognizer*)_recognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*)_otherRecognizer
{
return YES;
}
Inside the gesture recognizer handler, I'm trying to find the exact subview of the tap by using the hitTest method.
-(void)OnGesture:(UIGestureRecognizer*)_recognizer
{
if([_recognizer.state == UIGestureRecognizerStateEnded])
{
if([_recognizer isKindOfClass:[UITapGestureRecognizer class]]
|| [_recognizer isKindOfClass:[UILongPressGestureRecognizer class])
{
CGPoint location = [_recognizer locationOfTouch:0 inView:_recognizer.view];
// my problem occurs here:
//---------------------------------------------------------------------------
UIView* hitView = [_recognizer.view hitTest:location withEvent:nil];
//---------------------------------------------------------------------------
NSLog(#"Hit on view: %#", hitView);
}
}
}
So my problem is:
Sometimes (1 out of 10 cases) when I press the UIButton OnGesture method fires, but the IBAction of the "Touch Up Inside" event of that button is not firing.
But when I comment out hitTest call:
//UIView* hitView = [_recognizer.view hitTest:location withEvent:nil];
the bug stops being reproducible. IBAction always gets called.
Why is this happening? How can I fix this?
P.S. there could be some typos in the sample code above.
According to the docs, in order for it to work:
This method ignores view objects that are hidden, that have disabled user interactions, or have an alpha level less than 0.01. This method does not take the view’s content into account when determining a hit. Thus, a view can still be returned even if the specified point is in a transparent portion of that view’s content.
So you might wanna do self.someSubview.userInteractionEnabled = YES;
Related
I have a container view which has two UILongPressGestureRecognizers on it (well not only those two, but other gestures are acting ok). One is to recognize one finger, the other is for two. When i try to fire gesture for one finger, it will not fire it's target method until finger is lift off. The second one for two fingers is working as expected, firing selector action after short period of time. When i disable gesture for two finger press, the one finger press gets back to normal. How gestures are initialized, first is one finger gesture, initialized inside vc setup.
- (void) setupGestures
{
[self.contentView addGestureRecognizer:self.longGestureRecognizer];
}
- (UILongPressGestureRecognizer *)longGestureRecognizer
{
if (!_longGestureRecognizer) {
_longGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPressAction:)];
[_longGestureRecognizer setMinimumPressDuration:1];
}
return _longGestureRecognizer;
}
The second one is initalized a bit later, but not from host VC, but inside view which is added to it
//Called in host
rullerView = [[RullerView alloc] initWithContent:self.contentView viewController:self];
and in init of view:
//This is called from within previous call initWithContent:ciewController:
- (void)setupGestures
{
UILongPressGestureRecognizer *rullerGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(rullerTapAction:)];
[rullerGestureRecognizer setNumberOfTouchesRequired:2];
//view here is a content view passed into init, basically the same one
//to which first recognizer is added, however selector action is contained
//in this view, and this view is target for this recognizer
[view addGestureRecognizer:rullerGestureRecognizer];
}
Could this be a problem?? Already tried
[gesture requireGestureRecognizerToFail:otherGesture];
and delegate method of view which recognizers are added to:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
I have a custom subclass of MKPinAnnotationView that displays a custom call out. I want to handle touch events inside that annotation.
I have a working solution (below) but it just doesn't feel right. I have a rule of thumb that whenever I use performSelector: .. withDelay: I'm fighting the system rather than working with it.
Does anyone have a good, clean workaround for the aggressive event handling of MKMapView and annotation selection handling?
My current solution:
(All code from my annotation selection class)
I do my own hit testing (without this my gesture recognisers don't fire as the Map View consumes the events:
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event; {
// To enable our gesture recogniser to fire. we have to hit test and return the correct view for events inside the callout.
UIView* hitView = nil;
if (self.selected) {
// check if we tpped inside the custom view
if (CGRectContainsPoint(self.customView.frame, point))
hitView = self.customView;
}
if(hitView) {
// If we are performing a gesture recogniser (and hence want to consume the action)
// we need to turn off selections for the annotation temporarily
// the are re-enabled in the gesture recogniser.
self.selectionEnabled = NO;
// *1* The re-enable selection a moment later
[self performSelector:#selector(enableAnnotationSelection) withObject:nil afterDelay:kAnnotationSelectionDelay];
} else {
// We didn't hit test so pass up the chain.
hitView = [super hitTest:point withEvent:event];
}
return hitView;
}
Note that I also turn off selections so that in my overridden setSelected I can ignore the deselection.
- (void)setSelected:(BOOL)selected animated:(BOOL)animated; {
// If we have hit tested positive for one of our views with a gesture recogniser, temporarily
// disable selections through _selectionEnabled
if(!_selectionEnabled){
// Note that from here one, we are out of sync with the enclosing map view
// we're just displaying out callout even though it thinks we've been deselected
return;
}
if(selected) {
// deleted code to set up view here
[self addSubview:_customView];
_mainTapGestureRecogniser = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(calloutTapped:)];
[_customView addGestureRecognizer: _mainTapGestureRecogniser];
} else {
self.selected = NO;
[_customView removeFromSuperview];
}
}
It's the line commented 1 that I don't like but it's also pretty hairy at the end of theta delayed fire. I have to walk back up the superview chain to get to the mapView so that I can convince it the selection is still in place.
// Locate the mapview so that we can ensure it has the correct annotation selection state after we have ignored selections.
- (void)enableAnnotationSelection {
// This reenables the seelction state and resets the parent map view's idea of the
// correct selection i.e. us.
MKMapView* map = [self findMapView];
[map selectAnnotation:self.annotation animated:NO];
_selectionEnabled = YES;
}
with
-(MKMapView*)findMapView; {
UIView* view = [self superview];
while(!_mapView) {
if([view isKindOfClass:[MKMapView class]]) {
_mapView = (MKMapView*)view;
} else if ([view isKindOfClass:[UIWindow class]]){
return nil;
} else{
view = [view superview];
if(!view)
return nil;
}
}
return _mapView;
}
This all seems to work without and downside (like flicker I've seen from other solutions. It's relatively straightforward but it doesn't feel right.
Anyone have a better solution?
I don't think you need to monkey with the map view's selection tracking. If I'm correctly interpreting what you want, you should be able to accomplish it with just the hitTest:withEvent: override and canShowCallout set to NO. In setSelected: perform your callout appearance/disappearance animation accordingly. You should also override setHighlighted: and adjust the display of your custom callout if visible.
On my mapview I draw polygon overlays that belong to a specific annotation. I want that annotation to be selected when the overlay is tapped. My first attempt was to add a UITapGestureRecognizer to the mapview, test whether the tapped point is inside a polygon and perform [mapView selectAnnotation:myAnnotation] on success. The problem is that after this, the mapview decides there was a tap not on any annotations, so it deselects the annotation again.
My question is how to best prevent this from happening, I don't seem to be able to find a nice solution. What I have tried:
Create a new UIGestureRecognizer subclass that recognizes just taps inside overlays, then iterate through mapView.gestureRecognizers and call requireGestureRecognizerToFail on each. However, the mapview does not expose any recognizers through its property.
Return YES for shouldBeRequiredToFailByGestureRecognizer in my custom recognizer for any other recognizer that isKindOfClass tap recognizer. However, there still seems to be another recognizer that is not passed in there.
Place a transparent view on there and do the polygon check in pointInside:withEvent, but does also blocks any other gestures besides only taps.
EDIT:
After poking around a bit more, I have code that is almost working, of which I know where it goes wrong. I have a custom recognizer as before. In its delegate I do:
- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer
{
[otherGestureRecognizer requireGestureRecognizerToFail:gestureRecognizer]; // can possibly do this in custom recognizer itself instead
return YES;
}
Now taps inside polygons successfully prevent deselection. However, when I then do:
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView
{
// displayRegion is chosen to center annotation
[mapView setRegion:self.displayRegion animated:YES];
}
it breaks again, and the annotation gets deselected again..
It seems we have the same problem ( a little different: i'm tryng to select manually an annotation in a gesture recognizer )
I'm doing so ( and it works, but it seems to complex to me , feel free to ask more if it's not clear ):
i'm working with a long pressure event :
...
_lp1 = [[UILongPressGestureRecognizer alloc]
initWithTarget:self action:#selector(handleOverlayLp1:)];
((UILongPressGestureRecognizer*)_lp1).minimumPressDuration = 0.05;
_lp1.delegate = self;
[_mapView addGestureRecognizer:_lp1];
...
I collect all gesture recognizers in a global var :
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
if (_gestureRecognizers==nil)
_gestureRecognizers = [NSMutableSet set];
[_gestureRecognizers addObject:otherGestureRecognizer];
return YES;
}
// when i recognize gestures, disable everything and call an asyncrhronous task where i re-enable
- (void)handleOverlayLp1:(UIGestureRecognizer*)recognizer
{
// Do Your thing.
if (recognizer.state == UIGestureRecognizerStateBegan)
{
BOOL found=NO;
...
if (found) {
// disable gestures, this forces them to fail, and then reenable in selectOverlayAnnotation that is called asynchronously
for (UIGestureRecognizer *otherRecognizer in _gestureRecognizers) {
otherRecognizer.enabled = NO;
[self performSelector:#selector(selectOverlayAnnotation:) withObject:polyline afterDelay:0.1];
}
}
}
}
- (void)selectOverlayAnnotation: (id<MKAnnotation>) polyline
{
[_mapView selectAnnotation:polyline animated:NO];
for (UIGestureRecognizer *otherRecognizer in _gestureRecognizers) {
otherRecognizer.enabled = YES;
}
}
What I'm working with is I have a custom UIView that combines a UILabel with a UITextField as part of some user input. I have several of these throughout the view that is being displayed in the app. What would be good is to be able to have the user touch either the UILabel or the UITextField and then have the UITextField allow for input of the field.
Is there an easy way to do this?
Add a tap gesture recognizer to your label, and when tapped tell the corresponding textfield to become first responder (which brings up the keyboard).
// In your init or awakeFromNib:
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(labelTapped:)];
self.label.userInteractionEnabled = YES;
[self.label addGestureRecognizer:tap];
...
- (void)labelTapped:(id)sender
{
[self.textField becomeFirstResponder];
}
Yes, there is and I actually tested it. You can add a tap gesture recognizer to your view (the one that contains the label and the text view) like so:
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(viewTapped:)];
[self addGestureRecognizer:[tap autorelease]];
and then, inside viewTapped:
-(void)viewTapped:(id)sender {
[self.yourTextField becomeFirstResponder];
}
Hope this helps!
You can define UIGestureRecognizer delegate method and check that is the the required tap gesture area . If it is then return TRUE else return FALSE .
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
// Disallow recognition of tap gestures in the segmented control.
if ((touch.view == yourButton)) {//change it to your condition
return NO;
}
return YES;
}
i´ve made lots of my own "CustomUIButton" in a for-loop in my viewcontroller.
In this "CustomUIButton"-class i´ve implemented an UIGestureRecognizer like this:
(id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// custom things.
UILongPressGestureRecognizer* longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(handleLongPress:)];
longPress.minimumPressDuration = 1.0;
[self addGestureRecognizer:longPress];
[longPress release];
}
}
- (void) handleLongPress:(UILongPressGestureRecognizer*) recognizer{
if (recognizer.state == UIGestureRecognizerStateEnded) {
NSLog(#"Long press Ended");
}
else {
NSLog(#"Long press detected.");
// Do something
}
}
If i init the target with "self", my "handleLongPress"-function in this class will be called. It´s cool. If i init the target with "nil", it should check the parent viewcontroller, right?
Any ideas why an additional function with the same name in my viewcontroller won´t be called? (For this test i´ve commented the "longpress"-function of the button-class out.)
In the docs for UIGestureRecognizer's initWithTarget:action: method, for the target parameter it says:
An object that is the recipient of
action messages sent by the receiver
when it recognizes a gesture. nil is
not a valid value.
Note the last sentence.
The docs also say this which should explain why it doesn't work:
A gesture recognizer does not
participate in the view’s responder
chain.
You must specify a value for target.