UIPanGestureRecognizer on MKMapView? - objective-c

I would like to add some logic when user moves with map view i. e. he does a pan touch. But when I add the gesture recognizer and I want to log the touch, nothing happens. When I try it in another view controller and add the recognizer to controller's view then it works ok.
Here's my code (map view is a property of application delegate because I need to do some other things with it even if it isn't visible):
- (void)viewDidLoad
{
...
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(showPan)];
[appDelegate.mapView addGestureRecognizer:panGesture];
[panGesture release];
}
- (void)showPan
{
NSLog(#"pan!");
}
I use latest iOS 4.2.1
Thanks for any advice.

Ok, because no one knew, I had to spent one Apple technical support consult for it. ;o)
Because MKMapView evidently has its own recognizers to interact with user, you have to adhere to the UIGestureRecognizerDelegate protocol and implement (BOOL)gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: like this:
- (void)viewDidLoad
{
...
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(showPan)];
panGesture.delegate = self;
[appDelegate.mapView addGestureRecognizer:panGesture];
[panGesture release];
}
- (void)showPan
{
NSLog(#"pan!");
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
Then it works like a charm.

Swift 5
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGesture))
panGesture.delegate = self
self.mapView.addGestureRecognizer(panGesture)
#objc func panGesture (sender: UIPanGestureRecognizer) {
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
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;
}

How to disable back gesture in iOS 7 for only one view

I am trying to disable the back gesture for my view controller using the following set of code.
In FirstViewController.m, I'm setting the delegate of interactivePopGestureRecognizer
- (void) viewWillLoad {
// Other stuff..
self.navigationController.interactivePopGestureRecognizer.delegate = self;
}
And then implementing the <UIGestureRecognizerDelegate> method and returning NO.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
return NO;
}
And in dealloc I'm setting the delegate to nil. (I have read somewhere that in iOS 7, you have to manually set the delegates to nil)
- (void)dealloc {
self.navigationController.delegate = nil;
self.navigationController.interactivePopGestureRecognizer.delegate = nil;
}
This works in the FirstViewController. But when I push SecondViewController to this, the gesture does not work on that either. How can I disable the gesture in FirstViewController only?
Also when I pop FirstViewController to go to RootViewController and then try to push FirstViewController again, I get the object deallocated error :
[FirstViewController gestureRecognizer:shouldReceiveTouch:]: message sent to deallocated instance 0x14ed0280
Why else do I need to do other than setting the delegates to nil? Or am I setting it in the wrong place?
Try the below untested code in your FirstViewController :
-(void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}
-(void) viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
self.navigationController.interactivePopGestureRecognizer.enabled = YES;
}
I originally put these answers into a comment below the accepted answer, but I feel this needs to be said as an answer to get more visibility.
More often than not, you will find that the accepted answer does not work. This is because viewWillAppear: can be called before the view is added to a navigation controller's view hierarchy, and so self.navigationController is going to be nil. Because of this, the interactivePopGestureRecognizer may not be disabled in some cases. You're better off calling it in viewDidAppear: instead.
Here's code that will work (assuming your view controller is correctly added to a navigation controller's view hierarchy):
Objective-C
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[[[self navigationController] interactivePopGestureRecognizer] setEnabled:NO];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[[self navigationController] interactivePopGestureRecognizer] setEnabled:YES];
}
Swift
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationController?.interactivePopGestureRecognizer?.isEnabled = false
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.interactivePopGestureRecognizer?.isEnabled = true
}
I tried the all above but they did not work for me.So i tried this and it works for me on both IOS7 and IOS8.
Just make sure that your view controller implements this protocol i.e UIGestureRecognizerDelegate
and write the code given below.
-(void)viewWillAppear : (BOOL) animated {
[super viewWillAppear : animated];
if ([self.navigationController respondsToSelector:#selector(interactivePopGestureRecognizer)]) {
self.navigationController.interactivePopGestureRecognizer.enabled =
NO;
self.navigationController.interactivePopGestureRecognizer.delegate =
self;
}
}
-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
if ([gestureRecognizer isEqual:self.navigationController.interactivePopGestureRecognizer]) {
return NO;
} else {
return YES;
}
}
I found out setting the gesture to disabled only doesn't always work. It does work, but for me it only did after I once used the backgesture. Second time it wouldn't trigger the backgesture. Furthermore, as John Rogers said, it's import to use the viewDidAppear and viewWillAppear as the navigationController else would be nil.
Fix for me was to delegate the gesture and implement the shouldbegin method to return NO:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// Disable iOS 7 back gesture
if ([self.navigationController respondsToSelector:#selector(interactivePopGestureRecognizer)]) {
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
self.navigationController.interactivePopGestureRecognizer.delegate = self;
}
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// Enable iOS 7 back gesture
if ([self.navigationController respondsToSelector:#selector(interactivePopGestureRecognizer)]) {
self.navigationController.interactivePopGestureRecognizer.enabled = YES;
self.navigationController.interactivePopGestureRecognizer.delegate = nil;
}
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
return NO;
}
This just worked for me in xCode 7:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.navigationController!.interactivePopGestureRecognizer!.enabled = false
}
override func viewWillDisappear(animated: Bool) {
super.viewDidDisappear(animated)
self.navigationController!.interactivePopGestureRecognizer!.enabled = true
}
For only one view, I don't know the way... But I use the next code to disable fully the swipe gesture:
in your AppDelegate.m
if ([[UIDevice currentDevice].systemVersion floatValue] >= 7){
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}

CATransition also animating the navigationBar

I want to make a custom animation to pop my navigation controller. I only want to animate the view, not the navigationBar. With this code I animate both, the view and the navigationBar. How can I only animate the view??
CATransition* transition = [CATransition animation];
transition.duration = 0.3;
transition.type = kCATransitionFade;
transition.subtype = kCATransitionFromTop;
[self.navigationController.view.layer addAnimation:transition forKey:kCATransition];
[self.navigationController popViewControllerAnimated:NO];
this code is fired when a custom back button added inside the navigationcontroller bar is pressed.
Here is a code that does custom animation both for back button and when you call popRootViewController: method.
It's a class that extends UINavigationViewController which by itself contradicts Apple's docs also it assigns private variable using KVO, which might stop working as soon as engineers change UINavigationController class so use it a t your own risk.
#import "MyNavigationController.h"
#interface MyNavigationController () <UINavigationBarDelegate> {
// Flag that we will use to avoid collisions between navgiation bar
// when we call popViewControllerAnimated: method directly
BOOL _isPopping;
}
- (UIViewController *)myPopViewControllerAniamted:(BOOL)animated;
#end
#implementation MyNavigationController
- (id)init
{
self = [super init];
if (!self) return nil;
// We can't intercept delegation of the original navigation bar,
// we have to replace it with our own, by assigning new instance to
// the private _navigationBar vairable
UINavigationBar *navigationBar = [[UINavigationBar alloc] init];
navigationBar.delegate = self;
[self setValue:navigationBar forKey:#"_navigationBar"];
return self;
}
// This is the delegate method called when you're about to pop navigation item
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
// If we're in the process of popping items we don't want to reenter
if (!_isPopping) {
[self myPopViewControllerAniamted:YES];
}
return YES;
}
// Similarly we have to override popToRootViewControllerAnimated:
// The only difference would be that we use not previous view as a
// target for the transfition, but the very first view
- (UIViewController *)popViewControllerAnimated:(BOOL)animated
{
return [self myPopViewControllerAniamted:animated];
}
// Our custom popping method
- (UIViewController *)myPopViewControllerAniamted:(BOOL)animated
{
_isPopping = YES;
// If we got here, we have at least two view controllers in the stack
UIViewController *currentViewController = self.topViewController;
if (animated && self.viewControllers.count > 1) {
UIView *currentView = currentViewController.view;
UIViewController *previousViewController = [self.viewControllers objectAtIndex:self.viewControllers.count - 2];
UIView *previousView = previousViewController.view;
previousView.alpha = 0.0;
[currentView.superview insertSubview:previousView belowSubview:currentView];
// I use UIView just for the sake of the simplicity of this example
// In case of core animation you will have to deal with delegates
// to trigger view controller popping when animation finishes
[UIView animateWithDuration:0.33 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
currentView.alpha = 0.0;
previousView.alpha = 1.0;
} completion:^(BOOL finished) {
[super popViewControllerAnimated:NO];
_isPopping = NO;
}];
} else {
[super popViewControllerAnimated:NO];
_isPopping = NO;
}
return currentViewController;
}
#end
Once again, it was done purely as exercise of what is possible, I would highly recommend reading UIViewController guide, probably Container View Controller can satisfy you needs as a designated way of customizing view controller behaviour.
Hope it helps!

Custom MKMapView callout not responding to touch events

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;
}

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.