Custom MKMapView callout not responding to touch events - objective-c

I have a custom UIView that I am displaying as a callout when the user clicks on a custom annotation on an MKMapView.
To achieve this I have subclassed MKAnnotationView and overloaded the -setSelected:selected animated: method as suggested in this answer. Basically, I am adding my custom view as a subview of my MKAnnotationView subclass.
The problem is that I can't interact with the callout, which contains a button and a scrollable webview, at all. What's more, if the callout hides an annotation and I press the callout at the approximate location of that hidden annotation, the callout will get dismissed and a new one will be shown.
// TPMapAnnotationView.m
#implementation TPMapAnnotationView
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
[super setSelected:selected animated:animated];
if(selected)
{
TPMapAnnotation* anno = ((TPMapAnnotation*)self.annotation);
QuickInfoView* qi = [[QuickTabView alloc] initWithFrame:CGRectMake(0, 0, 440, 300)];
[qi displayDataForAnnotation:anno];
[self addSubview:qi];
// some animiation code that doesn't change things either way
}
else
{
[[self.subviews objectAtIndex:0] removeFromSuperview];
}
}
The code below creates the TPMapAnnotationView.
// this is in the view controller that contains the MKMapView
- (MKAnnotationView *) mapView:(MKMapView *) mapView viewForAnnotation:(id) annotation
{
if ([annotation isKindOfClass:[TPMapAnnotation class]])
{
TPMapAnnotationView *customAnnotationView = (TPMapAnnotationView *)[myMap dequeueReusableAnnotationViewWithIdentifier:#"TPAnn"];
if (customAnnotationView == nil)
{
customAnnotationView = [[TPMapAnnotationView alloc] initWithAnnotation:annotation
reuseIdentifier:#"TPAnn"];
}
[customAnnotationView setImage:annotationImage];
return customAnnotationView;
}
return nil; // blue radar circle for MKUserLocation class.
}

This is a well known problem. Anything you add on AnnotationView will not detect touches. There is good open source project for this problem. http://dev.tuyennguyen.ca/wp-content/uploads/2011/03/CustomMapAnnotationBlogPart1.zip, http://dev.tuyennguyen.ca/wp-content/uploads/2011/03/CustomMapAnnotationBlogPart21.zip
EDIT:
Yes. I also tried hard to add uibuttons to my own custom annotationView but then I stumbled upon this project and found that his custom annotationView is actually a annotation.
Anyway if you want to to change height of annotatioView then you can set
- (MKAnnotationView *) mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>) annotation{
calloutMapAnnotationView.contentHeight = height;
calloutMapAnnotationView.titleHeight = 25;
}
here, titleHeight is property added to CalloutMapAnnotationView which determines height of "gloss" in drawRectMethod
- (void)drawRect:(CGRect)rect {
glossRect.size.height = self.titleHeight;
}
if you are having any difficulty please let me know.
And also the link to original blogpost:
http://dev.tuyennguyen.ca/?p=298

I resolved this problem. Click anything in CalloutView,the map will not get touch.My calloutview is custom have tabbleview
1 - In file MapviewController.h you will add delegate : UIGestureRecognizerDelegate
2 - and in file MapViewController.m implement method - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
-In my mapView when you click 1 time on Map it will go in this method 3 time. So I limit touch will action.the first touch will action.
- In myCalloutView have tabbleView, if tabbleView receive touch It will return false touch for Map, it will make your tabbleview can get touch.It same for your button.
Note : in NSlog hit test View : will have name of view item you want it have touch.
example my view : isEqualToString:#"UITableViewCellContentView"]
static int count=0;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{
NSLog(#"hit test view %#",[touch view]);
if(count >0 && count<=2)
{
count++;
count=count%2;
return FALSE;
}
count++;
if ([[[[touch view] class] description] isEqualToString:#"UITableViewCellContentView"]) {
return FALSE;
}
return TRUE;
}

Related

check if a uiView has handled touch

What I want to do is put a UIView on top of another UIView, and both of them are screen size. The top UIView includes lots cocos nodes and will respond when I touch them. But when I touch a place that has no cocos node, the bottom UIView should respond.
I don't know how to do this. My imagine is check if top uiView is handled touch, do nothing. other wise let the bottom UIView start respond. But I don't know how to check that. I only know how to check touch but it seems the UIView will also be touched when I touch some place it can't handle.
I think you not requires 2 views.
Just take one view only and add all your node to that view.
in .h
IBOutlet UIView *bgView;//your view
UITapGestureRecognizer *viewTapRecognizer;// view tap recognizer
in .m
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
viewTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handelGesture:)];
[bgView addGestureRecognizer:viewTapRecognizer];
for (UIView *subView in [bgView subviews]) {
UITapGestureRecognizer *nodeTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handelGesture:)];
[subView addGestureRecognizer:nodeTapRecognizer];
}
}
- (void)handelGesture:(UITapGestureRecognizer*)sender {
if (sender == viewTapRecognizer) {
// your view is tapped
NSLog(#"........Tapped view..........");
}
else {
// your node is tapped
NSLog(#"........Tapped node..........");
}
}
Try this. it might work for you.
You can achieve it by 2 ways: using pointInside... method or hitTest... method.
If you will use pointInside... you can use only your views (the top view and the bottom view). You will subclass UIView with the top view and override pointInside... method. You will return YES if user's tapped a cocoa node else returns NO.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
BOOL inside = [self didUserTapCocoaNode:point];
return inside;
}
If you want more complex logic, you should use third view, subclass of UIView (the container view), that will be contain the top view and the bottom view. Override method hitTest..., and return the bottom or the top view.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = nil;
if ([self touchedCocoaNode:point])
{
result = self.topView;
}
else
{
result = self.bottomView;
}
return result;
}
UPD
Example implementation of didUserTapCocoaNode with pseudo code:
- (BOOL)didUserTapCocoaNode:(CGPoint)pointInSelf
{
__block BOOL tappedSomeNode = NO;
[self.nodes enumerateObjectsUsingBlock:^(NodeType* obj, NSInteger idx, BOOL* stop){
CGPoint pointInNode = [obj convertPoint:pointInSelf fromView:self];
tappedSomeNode = [obj pointInside:pointInNode withEvent:nil];
*stop = tappedSomeNode;
}]
return tappedSomeNode;
}

UISlider inside UIPageViewController

I have a PageViewController which is initialized like this:
self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil];
On one of the pages, there's a UISlider.
My problem is that when I have transitionstyle set to UIPageViewControllerTransitionStyleScroll, it takes 150-200 ms before beginTrackingWithTouch is invoked on the slider.
This behavior is not seen when I use UIPageViewControllerTransitionStylePageCurl, where the UISlider is selected instantly.
This means that unless the user waits a bit before dragging the slider (a video progress), the page will turn instead, which is far from ideal.
The Page curl animation does not meet the demands of the app, so any explanation or workaround is appreciated.
Since with UIPageViewControllerTransitionStyleScroll gesture recognizers isn't available, you can use this:
for (UIView *view in pageViewController.view.subviews) {
if ([view isKindOfClass:[UIScrollView class]]) {
UIScrollView *scrollView = (UIScrollView *)view;
scrollView.delaysContentTouches = NO;
}
}
I solved this issue by add a pan gesture on UISlider and set:
self.sliderGesture.cancelsTouchesInView = NO; // make touch always triggered
and implement delegate method like:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return otherGestureRecognizer.view.superview == self.parentViewController.view;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
// only receive touch in slider
CGPoint touchLocation = [touch locationInView:self.view];
return CGRectContainsPoint(self.slider.frame, touchLocation);
}
You can try to set the delegate of the page view controller gestures to the root view controller:
for (UIGestureRecognizer* gestureRecognizer in self.pageViewController.gestureRecognizers) {
gestureRecognizer.delegate = self;
}
And then prevent the touch of the gestures if it appears inside UISlider which is a subclass of UIControl:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
return ([touch.view isKindOfClass:[UIControl class]] == NO);
}
What helped me was to add pan-gesture-recognizer to UIView which holds UISlider, so in the end I have
UIPageViewController->UIScrollView->...->MyView->UISlider
The 'MyView' thing had pan gesture registered to it which did nothing, but served just to NOT propagate events to scroll view.

objective-c refactor method

I have the following code:
-(void)textFieldDidBeginEditing:(UITextField *)textField
{
CGRect textFieldRect = [self.view.window convertRect:textField.bounds fromView:textField];
CGRect viewRect = [self.view.window convertRect:self.view.bounds fromView:self.view];
...
}
As you can see it passes in a UITextField. I also have this code duplicated in the same ViewController but passing in a UITextView.
I want to be able to refactor this into a single method which passes in either the UITextField or UITextView? How can I do this?
Also this code appears in other View Controllers, so ideally I'd like to place it in a helper class, very new to iOS so not sure where to go from here.
I've removed most of the code from the method for brevity, but what it does is it slides the UI controls into view when the iOS keyboard appears.
You can expect UIView, as it seems you don't use any special text properties from those views.
-(void)textFieldDidBeginEditing:(UIView *)textField
{
CGRect textFieldRect = [self.view.window convertRect:textField.bounds fromView:textField];
CGRect viewRect = [self.view.window convertRect:self.view.bounds fromView:self.view];
// ...
}
Call a helper methods, that expects a UIView, the common super class.
-(void)textFieldDidBeginEditing:(UITextField *)textField
{
return [self textBeginEditing:textField];
}
-(void)textViewDidBeginEditing:(UITextView *)textView
{
return [self textBeginEditing:textView];
}
-(void)textBeginEditing:(UIView *)view
{
//and if you need to do something, where you need to now, if it is a textView or a field, use
if([view isKindOfClass:[UITextField class]]){
//…
} else if([view isKindOfClass:[UITextView class]]){
//…
}
}

Click through NSView

I have an NSView containing multiple subviews. One of those subviews is transparent and layered on top.
I need to be able to click through this view down to the subviews below (so that the view below gets first responder status), but all the mouse events get stuck on the top view (alpha is 1, because I draw stuff in it - so it should only click through transparent areas).
I actually expected this to work, since normally it does. What's wrong?
Here's another approach. It doesn't require creating a new window object and is simpler (and probably a bit more efficient) than the findNextSiblingBelowEventLocation: method above.
- (NSView *)hitTest:(NSPoint)aPoint
{
// pass-through events that don't hit one of the visible subviews
for (NSView *subView in [self subviews]) {
if (![subView isHidden] && [subView hitTest:aPoint])
return subView;
}
return nil;
}
I circumvented the issue with this code snippet.
- (NSView *)findNextSiblingBelowEventLocation:(NSEvent *)theEvent {
// Translate the event location to view coordinates
NSPoint location = [theEvent locationInWindow];
NSPoint convertedLocation = [self convertPointFromBase:location];
// Find next view below self
NSArray *siblings = [[self superview] subviews];
NSView *viewBelow = nil;
for (NSView *view in siblings) {
if (view != self) {
NSView *hitView = [view hitTest:convertedLocation];
if (hitView != nil) {
viewBelow = hitView;
}
}
}
return viewBelow;
}
- (void)mouseDown:(NSEvent *)theEvent {
NSView *viewBelow = [self findNextSiblingBelowEventLocation:theEvent];
if (viewBelow) {
[[self window] makeFirstResponder:viewBelow];
}
[super mouseDown:theEvent];
}
Here's a Swift 5 version of figelwump's answer:
public override func hitTest(_ point: NSPoint) -> NSView? {
// pass-through events that don't hit one of the visible subviews
return subviews.first { subview in
!subview.isHidden && nil != subview.hitTest(point)
}
}
Here's a Swift 5 version of Erik Aigner's answer:
public override func mouseDown(with event: NSEvent) {
// Translate the event location to view coordinates
let convertedLocation = self.convertFromBacking(event.locationInWindow)
if let viewBelow = self
.superview?
.subviews // Find next view below self
.lazy
.compactMap({ $0.hitTest(convertedLocation) })
.first
{
self.window?.makeFirstResponder(viewBelow)
}
super.mouseDown(with: event)
}
Put your transparent view in a child window of its own.

UIGestureRecognizer blocks subview for handling touch events

I'm trying to figure out how this is done the right way. I've tried to depict the situation:
I'm adding a UITableView as a subview of a UIView. The UIView responds to a tap- and pinchGestureRecognizer, but when doing so, the tableview stops reacting to those two gestures (it still reacts to swipes).
I've made it work with the following code, but it's obviously not a nice solution and I'm sure there is a better way. This is put in the UIView (the superview):
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if([super hitTest:point withEvent:event] == self) {
for (id gesture in self.gestureRecognizers) {
[gesture setEnabled:YES];
}
return self;
}
for (id gesture in self.gestureRecognizers) {
[gesture setEnabled:NO];
}
return [self.subviews lastObject];
}
I had a very similar problem and found my solution in this SO question. In summary, set yourself as the delegate for your UIGestureRecognizer and then check the targeted view before allowing your recognizer to process the touch. The relevant delegate method is:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldReceiveTouch:(UITouch *)touch
The blocking of touch events to subviews is the default behaviour. You can change this behaviour:
UITapGestureRecognizer *r = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(agentPickerTapped:)];
r.cancelsTouchesInView = NO;
[agentPicker addGestureRecognizer:r];
I was displaying a dropdown subview that had its own tableview. As a result, the touch.view would sometimes return classes like UITableViewCell. I had to step through the superclass(es) to ensure it was the subclass I thought it was:
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
UIView *view = touch.view;
while (view.class != UIView.class) {
// Check if superclass is of type dropdown
if (view.class == dropDown.class) { // dropDown is an ivar; replace with your own
NSLog(#"Is of type dropdown; returning NO");
return NO;
} else {
view = view.superview;
}
}
return YES;
}
Building on #Pin Shih Wang answer. We ignore all taps other than those on the view containing the tap gesture recognizer. All taps are forwarded to the view hierarchy as normal as we've set tapGestureRecognizer.cancelsTouchesInView = false. Here is the code in Swift3/4:
func ensureBackgroundTapDismissesKeyboard() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
tapGestureRecognizer.cancelsTouchesInView = false
self.view.addGestureRecognizer(tapGestureRecognizer)
}
#objc func handleTap(recognizer: UIGestureRecognizer) {
let location = recognizer.location(in: self.view)
let hitTestView = self.view.hitTest(location, with: UIEvent())
if hitTestView?.gestureRecognizers?.contains(recognizer) == .some(true) {
// I dismiss the keyboard on a tap on the scroll view
// REPLACE with own logic
self.view.endEditing(true)
}
}
One possibility is to subclass your gesture recognizer (if you haven't already) and override -touchesBegan:withEvent: such that it determines whether each touch began in an excluded subview and calls -ignoreTouch:forEvent: for that touch if it did.
Obviously, you'll also need to add a property to keep track of the excluded subview, or perhaps better, an array of excluded subviews.
It is possible to do without inherit any class.
you can check gestureRecognizers in gesture's callback selector
if view.gestureRecognizers not contains your gestureRecognizer,just ignore it
for example
- (void)viewDidLoad
{
UITapGestureRecognizer *singleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleSingleTap:)];
singleTapGesture.numberOfTapsRequired = 1;
}
check view.gestureRecognizers here
- (void)handleSingleTap:(UIGestureRecognizer *)gestureRecognizer
{
UIEvent *event = [[UIEvent alloc] init];
CGPoint location = [gestureRecognizer locationInView:self.view];
//check actually view you hit via hitTest
UIView *view = [self.view hitTest:location withEvent:event];
if ([view.gestureRecognizers containsObject:gestureRecognizer]) {
//your UIView
//do something
}
else {
//your UITableView or some thing else...
//ignore
}
}
I created a UIGestureRecognizer subclass designed for blocking all gesture recognizers attached to a superviews of a specific view.
It's part of my WEPopover project. You can find it here.
implement a delegate for all the recognizers of the parentView and put the gestureRecognizer method in the delegate that is responsible for simultaneous triggering of recognizers:
func gestureRecognizer(UIGestureRecognizer, shouldBeRequiredToFailByGestureRecognizer:UIGestureRecognizer) -> Bool {
if (otherGestureRecognizer.view.isDescendantOfView(gestureRecognizer.view)) {
return true
} else {
return false
}
}
U can use the fail methods if u want to make the children be triggered but not the parent recognizers:
https://developer.apple.com/reference/uikit/uigesturerecognizerdelegate
I was also doing a popover and this is how I did it
func didTap(sender: UITapGestureRecognizer) {
let tapLocation = sender.locationInView(tableView)
if let _ = tableView.indexPathForRowAtPoint(tapLocation) {
sender.cancelsTouchesInView = false
}
else {
delegate?.menuDimissed()
}
}
You can turn it off and on.... in my code i did something like this as i needed to turn it off when the keyboard was not showing, you can apply it to your situation:
call this is viewdidload etc:
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:#selector(notifyShowKeyboard:) name:UIKeyboardDidShowNotification object:nil];
[center addObserver:self selector:#selector(notifyHideKeyboard:) name:UIKeyboardWillHideNotification object:nil];
then create the two methods:
-(void) notifyShowKeyboard:(NSNotification *)inNotification
{
tap.enabled=true; // turn the gesture on
}
-(void) notifyHideKeyboard:(NSNotification *)inNotification
{
tap.enabled=false; //turn the gesture off so it wont consume the touch event
}
What this does is disables the tap. I had to turn tap into a instance variable and release it in dealloc though.