When a use selects an annotation in a map, I show a bottom view with informations, such as the google maps app.
I'm showing it in the map's delegate :
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
When the user deselects it (by taping anywhere on the map), I hide my bottom view. This is done in the opposite delegate method :
- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view
It works well, I'm happy with it. However, if a user selects a second annotation (ie: he taps a first annotation, then taps another one, without deselecting first the annotation in the meantime), I don't want to hide my bottom view then show it again. I'd just like to change informations in it.
However, as mapView:didDeselectAnnotationView: is called before mapView:didSelectAnnotationView:, I can't figure out how to detect the situation I'm describing above.
My question is : how can I detect that a user selects a second annotation OR, how should I solve this problem in any other way ?
This can be accomplished without adding an explicit delay by executing asynchronously on the main queue.
var currentSelection: MKAnnotationView?
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
currentSelection = view
*** select code goes here ***
}
func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
DispatchQueue.main.async { [weak self] in
self?.delayedDeselect(view: view)
}
}
func delayedDeselect(view: MKAnnotationView) {
if currentSelection == view {
*** deselect code goes here ***
}
}
Maybe try put a delay in your didDeselectAnnotationView method to hide your bottomView. You need to store a reference to your last selected annotation view though.
Example:
#interface MyViewController
{
MKAnnotationView *lastSelectedAnnotationView;
}
#end
#implementation MyViewController
...
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
...
[self updateBottomViewInfoWithAnnotationView:view];
lastSelectedAnnotationView = view;
}
- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view
{
// ------------------------------------------------------------------
// perform check to hide bottomView after a delay, to give
// didSelectAnnotationView a chance to select new annotation
// ------------------------------------------------------------------
[self performSelector:#selector(checkShouldHideBottomView:) withObject:view afterDelay:0.5];
}
-(void)checkShouldHideBottomView:(MKAnnotationView *)lastDeselectedAnnotationView
{
// ----------------------------------------------------------------------
// Only hide bottom view if user did not select a new annotation or
// last selected annotation is the same as the one being deselected
// ----------------------------------------------------------------------
if(lastSelectedAnnotationView == nil || lastDeselectedAnnotationView == lastSelectedAnnotationView)
{
// hide your bottom view example
self.bottomView.alpha = 0;
// clear lastSelectedAnnotationView reference
lastSelectedAnnotationView = nil;
}
}
Related
I am a beginner with XCode and I am trying to find a way for the User to input information inside a callbox of a pin they had placed on the map.
So if the User drops a pin and they tap on the pin, I want them to be able to type in a title and a subtitle of that location.
So far, I have created a map using MKMapView and I have implemented UILongPressGestureRecognizer and a void method that fires the moment a pin is created.
I am not sure what to do next? Thank you for your help.
Edit1: I don't have the required reputation to post an image, so here is a link of what I am trying to accomplish: http://imgur.com/gixFxpI
If it is possible to type in the title and subtitle without the use of a disclosure button, that would be swell. Later on I will develop the disclosure button.
Edit2: I have three new questions: 1. Why isn't my disclosure button appearing in my callout? 2. What is reuseidentifier for? Here is what I have so far.
I have embedded a navigation controller to my initial view controller that has the MKMapView in it. I created a second View Controller and I connected the MKMapView to the second View Controller and made my identifier for the second View Controller as 'heylisten' (good old Zelda).
My third question is in the prepareForSegue method, what information needs to go after the dot? destinationViewController.???? = sender.annotation;
// This is your callout box
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
{
MKAnnotationView *annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:#"overhere"];
annotationView.canShowCallout = YES;
UIButton *rightDisclosureButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
annotationView.rightCalloutAccessoryView = rightDisclosureButton;
return annotationView;
}
// This is a segue when you tap the right disclosure button
- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
[self performSegueWithIdentifier:#"heylisten" sender:view];
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(MKAnnotationView *)sender
{
if ([segue.identifier isEqualToString:#"heylisten"])
{
ViewController *destinationViewController = segue.destinationViewController;
// grab the annotation from the sender
destinationViewController = sender.annotation;
}
}
If you're targeting iOS 8, use a UIAlertController:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIAlertController_class/
If you're targeting iOS 7 or earlier, use a UIAlertView:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIAlertView_Class/index.html#//apple_ref/occ/cl/UIAlertView
Both of these types have mechanisms to add an input text field and specify a callback to be executed with the text the user typed.
I have two events, one fires on selection of a pin.
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKPinAnnotationView *)view { // when they click on a pin
if([view.annotation isKindOfClass: [MKUserLocation class]] || pinSelected == TRUE) // if its the users location ignore it or if pin already selected
{
return;
}
pinSelected = true;
view.image = [UIImage imageNamed:#"map_pin_selected.png"]; // change the image to selected
[self HideShowDetailsMenu:[view.annotation.title integerValue] ShowDetails:true];
}
which changes the pin to selected and makes a details menu appear with data relating to the selected pin and another which fires on deselecting a pin
- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKPinAnnotationView *)view { // when they click off a pin
if([view.annotation isKindOfClass: [MKUserLocation class]] || pinSelected == FALSE) // if its the users location ignore it or if pin already selected
return;
NSLog(#"This is running");
if ([view.annotation.subtitle integerValue] == [AppConstants Instance].CurrentStore.storeid)
view.image = [UIImage imageNamed:#"map_pin_starred.png"]; // set the pin to the unselected image
else
view.image = [UIImage imageNamed:#"map_pin_unselected.png"]; // change the pin to unselected
pinSelected = false;
[self HideShowDetailsMenu:[view.annotation.title integerValue] ShowDetails:false]; // pin unselected so hide the details menu
}
which will happen when a user deselects a Pin on the MapView.
However my issue that is occuring is that when I select a pin that has already been selected my program crashes, I am trying to circumvent this by implementing a bool that is set to true if a pin is set, which will just 'return' and do nothing if the event is triggered again.
However my issue is that the 'didDeselectAnnotationView' event will trigger when I select a pin that has been selected, even though I am not deselecting a pin at all. This triggers the 'pinSelected' variable to be set to false, then the 'didSelectAnnotationView' even is triggered which with the 'pinSelected' variable set to false will then run again crashing the program.
Have I done something wrong? What can I do in this situation?
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)aView
{
indexPathTag=aView.tag;
[mapView deselectAnnotation:aView.annotation animated:YES];
}
- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)aView
{
}
When didSelectAnnotationView fires, it actually tags the annotation as selected somehow. Then when you click it again, the delegate function doesn't fire because it is 'already selected'. You have to manually deselect the annotation by calling the following function once you're done doing what you want.
So you have to Add [mapView deselectAnnotation:aView.annotation animated:YES]; in didSelectAnnotationView delegate method.
I have a UITapGestureRecognizer that will hide and show a toolbar over my MKMap when the user taps the Map - simple.
However, when the user taps on an MKMapAnnotation, I do not want the map to respond to a tap in the normal way (above). Additionally, when the user taps elsewhere on the map to de-select an MKAnnotation callout, I also don't want the toolbar to respond. So, the toolbar should only respond when there are no MKAnnotations currently in selected state. Nor should it respond when the user clicks on an annotation directly.
So far, I have being trying the following action that reacts to the tap gesture on the map - however the Annotation View is never detected (the first if statement) and also, the annotation view is also launched regardless of this method.
-(void)mapViewTapped:(UITapGestureRecognizer *)tgr
{
CGPoint p = [tgr locationInView:self.mapView];
UIView *v = [self.mapView hitTest:p withEvent:nil];
id<MKAnnotation> ann = nil;
if ([v isKindOfClass:[MKAnnotationView class]])<---- THIS CONDITION IS NEVER MET BUT ANNOTATIONS ARE SELECTED ANYWAY
{
//annotation view was tapped, select it…
ann = ((AircraftAnnotationView *)v).annotation;
[self.mapView selectAnnotation:ann animated:YES];
}
else
{
//annotation view was not tapped, deselect if some ann is selected...
if (self.mapView.selectedAnnotations.count != 0)
{
ann = [self.mapView.selectedAnnotations objectAtIndex:0];
[self.mapView deselectAnnotation:ann animated:YES];
}
// If no annotation view is selected currently then assume control of
// the navigation bar.
else{
[self showToolBar:self.navigationController.toolbar.hidden];
}
}
}
I need to control the launch of the annotation call out programmatically and detect when the tap event has hit an annotation in order to achieve this.
Any help would be appreciated.
I think you will find the following links very useful:
http://blog.asynchrony.com/2010/09/building-custom-map-annotation-callouts-part-2/
How do I make a MKAnnotationView touch sensitive?
The first link discusses (among other things) how to prevent the propagation of touches to the annotations so that they selectively respond, and the second one how to detect the touches.
I think that because MKMapAnnotationView are on top of MKMapView, they will get the touch event and respond to it (be selected) so I don't think you need to select your annotation manually.
Then, if you have a look at Advanced Gesture Recognizer WWDC 2010 video, you will see that your MKMapView will receive tap event anyway, even if it's below the annotation view. That's probably why your -(void)mapViewTapped:(UITapGestureRecognizer *)tgr method get called.
Apart from that, I can't see why your if ([v isKindOfClass:[MKAnnotationView class]]) is never true. I do the exact same thing in my code and it works fine!
Finally, to answer your last question, if you don't want to do anything when the user is just trying to close the callout, you could keep track of a custom isCalloutOpen boolean value like this:
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view {
//some code
_isCalloutOpen = YES;
}
- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view {
// delay the reset because didDeselectAnnotationView could (and is often) called before your gesture recgnizer handler method get called.
[self performSelector:#selector(resetCalloutOpenState) withObject:Nil afterDelay:0.1];
}
- (void)resetCalloutOpenState {
_isCalloutOpen = NO;
}
- (void)mapViewTapped:(UITapGestureRecognizer *)tgr {
if (_isCalloutOpen) {
return;
}
}
Titles and subtitles can be added to the user location that iOS shows using MKUserLocation. When the user taps on the location, these will show in a bubble above the location. The thought bubbles for other annotations can be shown by selecting the annotation with setSelected:animated: from MKAnnotationView. Unfortunately, MKUserLocation does not descend from MKAnnotationView.
How can I programmatically select the user location so the annotation appears over the user location without the user first tapping on it?
The documentation for MKAnnotationView says this about its setSelected:animated: method (and something similar for its selected property):
You should not call this method directly.
Instead, use the MKMapView method selectAnnotation:animated:. If you call it in the didAddAnnotationViews delegate method, you can be sure the annotation view is ready to show the callout otherwise calling selectAnnotation will do nothing.
For example:
-(void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views
{
for (MKAnnotationView *av in views)
{
if ([av.annotation isKindOfClass:[MKUserLocation class]])
{
[mapView selectAnnotation:av.annotation animated:NO];
//Setting animated to YES for the user location
//gives strange results so setting it to NO.
return;
}
}
}
Ok, so I have a map view that has a bunch of annotations on it. Certain annotations when selected need to display extended info in a small table view which i am doing by resizing the mapview to half screen and animating into view a table in the bottom half. If another annotation is selected that doesn't need the extra info then in the didDeselectAnnotationView: method i hide the table and go back to the full map view, rinse and repeat.. So far so good, everything is working great.
The issue i am having though is that if a user selects another annotation while they currently have an annotation selected then didSelectAnnotationView delegate method gets called BEFORE the didDeselectAnnotationView.
This is obviously a problem because i am using these two methods to decide whether or not i need to display/hide the info table below the mapview, see code below:
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
if ([view.annotation isKindOfClass:[MapLocation class]])
{
if ([self.selectedAnnotation numberOfEvents] == 1)
{
mapTableViewIsVisible = NO;
}
else if ([self.selectedAnnotation numberOfEvents] > 1)
{
// launch mini tableview
mapTableViewIsVisible = YES;
}
[self loadMapTableViewWithEvents:self.selectedAnnotation.events
forAnnotation:self.selectedAnnotation];
}
}
- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view
{
if ([view.annotation isKindOfClass:[MapLocation class]])
{
mapTableViewIsVisible = NO;
[self loadMapTableViewWithEvents:nil forAnnotation:(MapLocation*)view.annotation];
}
}
So for example if i select an annotation that needs the maptable and i currently have a regular annotation selected then the mapTable is loaded when the didSelectAnnotationView method above is called, however it is immediately hidden again because the didDeselectAnnotationView is called right after.
So far i havent been able to figure out a way to fix this.
Any ideas??
You could check for the case where no annotations are visible in didDeselectAnnotationView and then clean up your tableview on this case only. As all other cases will be handled by didSelectAnnotation view.
Something like:
- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view{
if([[mapView selectedAnnotations] count]==0){
mapTableViewIsVisible = NO;
[self loadMapTableViewWithEvents:nil forAnnotation:(MapLocation*)view.annotation];
}
}