Reputation: 17170
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?
Upvotes: 5
Views: 602
Reputation: 7416
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.
Upvotes: 1