Why calloutAccessoryControlTapped is called for tap on annotation view also? - ios

I have a map on my view controller and I don't know why but the delegate calloutAccessoryControlTapped is also called when I just tap on annotation view, not only when I tap on detail closure. So why this behavior?
import UIKit
import MapKit
extension MapVC: MKMapViewDelegate, CLLocationManagerDelegate
{
func mapView(mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl)
{
...
}
}

As per Apple developer docs:
Accessory views contain custom content and are positioned on either
side of the annotation title text. If a view you specify is a
descendant of the UIControl class, the map view calls this method as a
convenience whenever the user taps your view. You can use this method
to respond to taps and perform any actions associated with that
control. For example, if your control displayed additional information
about the annotation, you could use this method to present a modal
panel with that information.
If your custom accessory views are not descendants of the UIControl
class, the map view does not call this method.
So is your accessory view inherited from UIControl?
Reference:
https://developer.apple.com/library/ios/documentation/MapKit/Reference/MKMapViewDelegate_Protocol/#//apple_ref/occ/intfm/MKMapViewDelegate/mapView:annotationView:calloutAccessoryControlTapped:

Massimo Polimeni seems to be correct. There appears to be a problem with the rightCalloutAccessoryView, but not with the leftCalloutAccessoryView.
The code below (with a leftCalloutAccessoryView) works as expected. If you tap the left accessory, it prints "left accessory selected". If you tap the callout title, it doesn't print anything.
If you use a rightCalloutAccessoryView (commented out, below) and tap the right accessory, it prints "right accessory selected". If you tap the callout title, it also prints "right accessory selected". This does not seem correct.
import UIKit
import MapKit
class ViewController: UIViewController, MKMapViewDelegate {
#IBOutlet weak var mapView: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
let annotation = MKPointAnnotation()
annotation.coordinate = CLLocationCoordinate2D(latitude: 50.29, longitude: -107.79)
annotation.title = "Swift Current"
mapView.addAnnotation(annotation)
mapView.mapType = .standard
mapView.delegate = self
mapView.region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: annotation.coordinate.latitude,
longitude: annotation.coordinate.longitude),
span: MKCoordinateSpan(latitudeDelta: 1.0, longitudeDelta: 1.0)
)
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let reuseId = "Annotation"
var view = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId)
if view == nil {
view = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
view?.canShowCallout = true
view?.leftCalloutAccessoryView = UIButton(type: .detailDisclosure)
//view?.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
} else {
view?.annotation = annotation
}
return view
}
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
if control == view.leftCalloutAccessoryView {
print("left accessory selected")
} else if control == view.rightCalloutAccessoryView {
print("right accessory selected")
}
}
}

It depends what kind of button you set on the rightCalloutAccessoryView. E.g. if you use:
[UIButton buttonWithType:UIButtonTypeDetailDisclosure];
Then both tapping the callout and tapping the button both result in calloutAccessoryControlTapped being called with the button. But if you use:
[UIButton systemButtonWithImage:[UIImage systemImageNamed:#"info.circle"] target:nil action:nil];
Then only tapping the button will work, tapping the callout will be disabled.
If you have a custom button and you want the first behavior then you can make a button subclass and do this:
#interface UIButton2 : UIButton
#end
#implementation UIButton2
- (id)_mapkit_accessoryControlToExtendWithCallout{
return self;
}
#end
This private method is how it decides wether a button should also work for tapping the callout (learned this using Hopper). The default implementation checks self.buttonType to decide.
A more sensible way would be to start with the disclosure button and change its image, e.g.
UIButton *button = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
UIImage *image = [UIImage systemImageNamed:#"chevron.right" withConfiguration:[UIImageSymbolConfiguration configurationWithScale:UIImageSymbolScaleSmall];
[button setImage:image forState:UIControlStateNormal];
A lot of this weird behaviour is likely to do with changes in how callout buttons work since iOS 6.

Related

Button inside default callout view stops responding to touches

So I have a map with tons of annotations. Each annotation has the default callout view attached with a button I created that supposedly takes the user to another view controller. This button works fine at first but for some annotations, it does not register touches unless I zoom in on the annotation or click the annotation again. I am very lost. Here is my code.
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
var annotationView = MKAnnotationView()
guard let annotation = annotation as? LocationAnnotation else {return nil}
var identifier = ""
switch annotation.type {
case .nightclub:
identifier = "Nightclub"
case .hookahLounge:
identifier = "Hookah Lounge"
case .bar:
identifier = "Bar"
case .bowling:
identifier = "Bowling Alley"
case .arcade:
identifier = "Arcade"
case .pool:
identifier = "Pool"
}
if let dequedView = mapSF.dequeueReusableAnnotationView(withIdentifier: identifier) {
annotationView = dequedView
} else {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: identifier)
}
annotationView.canShowCallout = true
annotationView.isEnabled = true
let button = UIButton()
button.frame = CGRect(x: 0.0, y: 0.0, width: 30.0, height: 30.0)
let image = UIImage(named: "go")
button.setImage(image, for: .normal)
annotationView.detailCalloutAccessoryView?.backgroundColor = UIColor.darkGray
annotationView.rightCalloutAccessoryView = button
}
And here is the callout accessory function which does work initially but does not work at random times.
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
print("true button tapped")
}
Again my issue is that print statement does not execute for various annotations. My console would print that statement out every time I press the button in the callout but for some other times it does not.I have no idea why. Any help would be appreciated as this is one of my last remaining bugs for my app.
This is somewhat old for me now but I DID find a solution. In order to receive touches on the button in my callout, I had to simply add one line annotationView.isUserInteractionEnabled = false. And my image in my callout button was interactive regardless of if the map was zoomed in or not at all times. What a weird scenario but it works swimmingly.

How to show a UIView just above the MKpointAnnotation Pin

I want to show a UIView just above the pin location and if the user moves around the map the UIView should remain above the pin location. I dont want to use the callout bubble. Is there any other way?
in iOS 9 we have a new property named detailCalloutAccessoryView
You can create a view and set as
annotationView.detailCalloutAccessoryView = tempView
Please check the link to get more details
MapKit iOS 9 detailCalloutAccessoryView usage
Try using this code:
func mapView(_ mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView {
if (annotation is MKUserLocation) {
return nil
}
else if (annotation is YourAnnotationClassHere) {
let identifier = "MyCustomAnnotation"
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
if annotationView {
annotationView.annotation = annotation
}
else {
annotationView = MKAnnotationView(annotation, reuseIdentifier: identifier)
}
annotationView.canShowCallout = false
// set to YES if using customized rendition of standard callout; set to NO if creating your own callout from scratch
annotationView.image = UIImage(named: "your-image-here.png")!
return annotationView
}
return nil
}
That's mainly what you need for that to work. This is a swift version of this answer right here: How to create Custom MKAnnotationView and custom annotation title and subtitle
Hope it helped!!!

How do I trigger a tapped event (on a map pin) without using callouts?

Is there a way to trigger a tapped event (on a map pin) without the use of callouts?
I tried to implement the didSelectAnnotationView below, but it did not seem to work:
func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
if let annotation = annotation {
let identifier = "pin"
var view: MKPinAnnotationView
if let dequeuedView = self.mapView.dequeueReusableAnnotationViewWithIdentifier(identifier) as? MKPinAnnotationView {
dequeuedView.annotation = annotation
view = dequeuedView
} else {
view = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
view.canShowCallout = false
view.enabled = true
}
return view
}
return nil
}
func mapView(mapView: MKMapView!, didSelectAnnotationView view: MKAnnotationView!) {
println("test")
}
The didSelectAnnotationView is the correct method. If you're not seeing it called, it's likely that the map view's delegate has not been set.
The default viewForAnnotation behavior will render something very close to what yours does, so it might not be immediately obvious that the delegate wasn't set properly. You might want to put a log/breakpoint in viewForAnnotation (or do something that makes it more visually distinct, e.g. different pin color) and confirm whether the delegate methods are getting called at all.

MKPinAnnotationView with tappable callout

I am trying to create a pin annotation with callout that is clickable as whole part. I have written it like this, but it doesn't as expected - you need to tap on the button for mapView(mapView: MKMapView!, annotationView view: MKAnnotationView!, calloutAccessoryControlTapped control: UIControl!) to be called
func mapView(mapView: MKMapView!, viewForAnnotation annotation: JJMapAnnotation!) -> MKAnnotationView! {
if annotation.isKindOfClass(MKUserLocation) {
return nil
}
var annotationIdentifier = "AnnotationIdentifier";
var pinView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: annotationIdentifier)
var rightButton = UIButton.buttonWithType(UIButtonType.DetailDisclosure) as UIButton
pinView.rightCalloutAccessoryView = rightButton;
pinView.animatesDrop = true;
pinView.canShowCallout = true;
return pinView
}
Which gives me this:
I think it should be possible, because that is the behavior I am observing with Apple maps. You can click wherever on their annotation's callout to be taken to detail view.
Also - when I click anywhere besides the button on callout it behaves as a button - it highlights for the time of tap. But the calloutAccessoryControlTapped won't be called and I didn't find any API function that could be called for that event.
I tried with the approach of adding some UIGestureRecognizers, but that doesn't really work considering the callout view is created in flight and is not an annotation view, so there is really no place where to hook it.
Edit: JJMapAnnotation is a subclass of MKPointAnnotation

How to always show map view annotation callouts?

How do you always show the annotation callouts, i.e. don't hide them when you tab the map view?
Resetting the annotations also will bring the callout to view state true.
[mapView removeAnnotation: currentMarker];
[mapView addAnnotation:currentMarker];
The callout is shown when an MKAnnotationView is selected and the view's canShowCallout property is set to YES.
It is then hidden when that MKAnnotationView is deselected. This occurs by tapping another annotation view, or by tapping outside of the currently selected annotation view.
As the delegate of MKMapView (conforming to MKMapViewDelegate), you are told when an annotation view is selected and deselected, but it's too late to do anything about it.
If you want to not deselect an annotation view, you should subclass MKAnnotationView and override the setSelected:animated: method and stop the annotation view from being deselected.
Thanks, #Zumry Mohammed for this idea. This solution in swift works for me:
func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
guard let ann = view.annotation else {return}
mapView.removeAnnotation(ann)
mapView.addAnnotation(ann)
mapView.selectAnnotation(ann, animated: false)
}
I just set isSelected property to true on viewFor annotation method and that is all.
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation {
return nil
}
let annotationV = MKAnnotationView(annotation: annotation, reuseIdentifier: nil)
annotationV.image = UIImage(named: "ZeusSurveyMarkerTaskIcon", in: Bundle(for: ZsurveysGeofenceLocationMapView.self), compatibleWith: nil)
annotationV.canShowCallout = true
annotationV.isSelected = true
return annotationV
}

Resources