Don't animate MKMarkerAnnotationView when selected - ios

I would like to capture taps on an MKMarkerAnnotationView, and not animate the view when this happens.
Through a delegate for my MKMapView, I can capture selection and deselection that are roughly equivalent to taps on the MKMarkerAnnotationView (the selection also happens when tapping the label that is not part of the MKMarkerAnnotationView)
I am trying to remove the default animation. I didn't find a straightforward solution.
I tried:
1/ setting the view to not selected during the selection. This does cancel the animation but further taps are not captured.
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
view.setSelected(false, animated: false)
// or
view.isSelected = false
handleTap(view)
}
2/ adding another tap gesture recognizer on the view and preventing other gesture recognizers from receiving the touch. This works well unless I tap the label instead of the annotation view.
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let view = dequeueReusableAnnotationView(withIdentifier: reuseIdentifier) as? MKMarkerAnnotationView ??
MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
view.annotation = annotation
view.gestureRecognizers?.forEach { view.removeGestureRecognizer($0) }
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTap(gesture:)))
gestureRecognizer.delegate = self
view.addGestureRecognizer(gestureRecognizer)
return view
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}

You can try override class:
final class CustomMarkerAnnotationView: MKMarkerAnnotationView {
var onSelect: (() -> Void)?
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(false, animated: false)
if selected {
onSelect?() // or catch this in delegate
}
}
}

Overriding did not work for me. Here's what ultimately worked for me (Objc):
- (void)mapView:(MKMapView *)mapView
didSelectAnnotationView:(MKAnnotationView *)view
{
//stops animation
for( CALayer *sublayer in view.layer.sublayers )
{
[sublayer removeAllAnimations];
}
//stops selection
[view setSelected:NO animated:NO];
}
For my own purposes I used the below instead, as it allows for properly deselecting the pin when tapping the pin again immediately after selection.
- (void)mapView:(MKMapView *)mapView
didSelectAnnotationView:(MKAnnotationView *)view
{
//stops animation
for( CALayer *sublayer in view.layer.sublayers )
{
[sublayer removeAllAnimations];
}
//stops selection
[mapView deselectAnnotation:view.annotation animated:NO];
}

Related

Detect on calloutAccessoryControlTapped only the tap on rightCalloutAccessoryView

My calloutAccessoryControlTapped is also called when I just tap on annotation view and this behavior it's right. But how can I detect if the user has tapped on the right accessory view (in my case a detail disclosure button) and not just in the view?
I added a simple check but it doesn't work.
import UIKit
import MapKit
extension MapVC: MKMapViewDelegate, CLLocationManagerDelegate
{
func mapView(mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl)
{
if control == view.rightCalloutAccessoryView
{
... // enter here even if I tapped on the view annotation and not on button
}
}
}
To achieve it you would need to add target for the right accessory view. You can achieve it by setting button to rightCalloutAccessoryView as shown in the code snippet.
class MapViewController: UIViewController, MKMapViewDelegate {
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is Annotation {
let annotationView = AnnotationView(annotation: annotation, reuseIdentifier: "reuseIdentifier")
let rightButton = UIButton(type: .DetailDisclosure)
rightButton.addTarget(self, action: #selector(didClickDetailDisclosure(_:)), forControlEvents: .TouchUpInside)
annotationView.rightCalloutAccessoryView = rightButton
}
return nil
}
func didClickDetailDisclosure(button: UIButton) {
// TODO: Perform action when was clicked on right callout accessory view.
}
}
// Helper classes.
class Annotation: NSObject, MKAnnotation {
var coordinate: CLLocationCoordinate2D
var title: String?
var subtitle: String?
init(coordinate: CLLocationCoordinate2D, title: String, subtitle: String) {
self.coordinate = coordinate
self.title = title
self.subtitle = subtitle
}
}
class AnnotationView: MKAnnotationView {
}
Using UIView and UITapGestureRecognizer instead of UIControl
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
let annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "reuseIdentifier")
let gestureView = UIView(frame:CGRect(x: 0,y: 0,width: 20,height: 20))
let gestureRecognizer = UITapGestureRecognizer()
gestureRecognizer.addTarget(self, action: #selector(MapViewController.didClickGestureRecognizer(_:)))
gestureView.addGestureRecognizer(gestureRecognizer)
gestureView.backgroundColor = UIColor.redColor()
annotationView.rightCalloutAccessoryView = gestureView
return annotationView
}
func didClickGestureRecognizer(sender:UITapGestureRecognizer) -> Void {
print("didClickGestureRecognizer")
}
When you click on rightCalloutAccessoryView, only didClickGestureRecognizer will be called,but your calloutAccessoryControlTapped cannot be invoked anyone.
2.If you have a UIControl as rightCalloutAccessoryView,you can tap directly on MKAnnotationView.Otherwise MKAnnotationView cannot be tapped.
Both your selector and calloutAccessoryControlTapped will be called when you tap rightCalloutAccessoryView or tap directly on MKAnnotationView
3.If you have a UIControl as leftCalloutAccessoryView,both your selector and calloutAccessoryControlTapped will be called when you tapped on it.
4.Since iOS 9,you can has a detailCalloutAccessoryView in your MKAnnotationView.Only your selector will be called when you tapped on it.
5.Also you can create your own custom MKAnnotationView,and change its behaviors.

Custom CalloutView with Swift 2.0 Best Approach?

I want to create a custom CalloutView from xib. I created a xib and custom AnnotationView class. I want to use my MapViewController segue which name is showDetail segue because this segue is a Push segue. When the user taps the button in my calloutView it should perform my showDetail segue.
I searched all documents, tutorials, guides and questions but I could
not find a any solution with Swift. There are no custom libraries also. I couldn't find any solution it as been 3 days.
My CalloutView.xib file View is my CustomAnnotationView which name is CalloutAnnotationView.swift and File's Owner is MapViewController it allows me to use MapViewController segues.
There is my CalloutAnnotationView.swift
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, withEvent: event)
if (hitView != nil) {
self.superview?.bringSubviewToFront(self)
}
return hitView
}
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
let rect = self.bounds
var isInside = CGRectContainsPoint(rect, point)
if (!isInside) {
for view in self.subviews {
isInside = CGRectContainsPoint(view.frame, point)
if (isInside) {
}
}
}
return isInside
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
let calloutViewController = MapViewController(nibName: "CalloutView", bundle: nil)
if selected {
calloutViewController.view.clipsToBounds = true
calloutViewController.view.layer.cornerRadius = 10
calloutViewController.view.center = CGPointMake(self.bounds.size.width * 0.5, -calloutViewController.view.bounds.size.height * 0.5)
self.addSubview(calloutViewController.view)
} else {
// Dismiss View
calloutViewController.view.removeFromSuperview()
}
}
My MapViewController.swift codes;
// MARK: - MapKit Delegate Methods
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
if let annotation = annotation as? Veterinary { // Checking annotations is not User Location.
var pinView = mapView.dequeueReusableAnnotationViewWithIdentifier(Constants.MapViewAnnotationViewReuseIdentifier) as? CalloutAnnotationView
if pinView == nil { // If it is nil it created new Pin View
pinView = CalloutAnnotationView(annotation: annotation, reuseIdentifier: Constants.MapViewAnnotationViewReuseIdentifier)
pinView?.canShowCallout = false
} else { pinView?.annotation = annotation } // If it is NOT nil it uses old annotations.
// Checking pin colors from Veterinary Class PinColor Method
pinView?.image = annotation.pinColors()
//pinView?.rightCalloutAccessoryView = UIButton(type: .DetailDisclosure)
return pinView
}
return nil
}
These codes are working fine to load my xib file. I created IBAction from a button and It performs segues from MapViewControllerto my DetailViewController with showDetail segue. But problem is It doesn't remove my customCalloutView from superview so I can't dismiss callout views.
How can I solve this problem or What is the best way to create custom calloutView for use my MapViewController push segues ?
Thank you very much.
In your MKMapView delegate see if this gets called when segueing:
func mapView(mapView: MKMapView, didDeselectAnnotationView view: MKAnnotationView) {
if let annotation = view.annotation as? Veterinary {
mapView.removeAnnotation(annotation)
}
}
I hope this helps you with the problem
func mapView(mapView: MKMapView, didDeselectAnnotationView view: MKAnnotationView) {
if view.isKindOfClass(AnnotationView)
{
for subview in view.subviews
{
subview.removeFromSuperview()
}
}
}

changing added MKAnnotationView alpha in didAddAnnotation affects all MKAnnotationViews on screen

I'm trying to fade my annotations in through animation, so I create them with an alpha of 0, and then animate their alpha to 1 in didAddAnnotation. Sometimes I'm only adding a few annotations, subtracting others, but right now EVERY annotation on screen is being faded out/in when ANY are added rather than the expected behavior, which would be that only the recently added pins would fade to 1.
- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views{
for (MKAnnotationView *view in views){
if (view.alpha ==0){
[UIView animateWithDuration:.5 animations:^{
view.alpha = 1;
}];
}
}
For animating the annotations as its added, you can use this subclass of MKAnnotationView
class AnnotationView: MKAnnotationView {
override var annotation: MKAnnotation? {
didSet {
UIView.animate(withDuration: 0.5) { self.alpha = 1.0 }
}
}
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
self.alpha = 0
}
override func prepareForReuse() {
super.prepareForReuse()
self.alpha = 0.0
}
}
Usage:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if let view = mapView.dequeueReusableAnnotationView(withIdentifier: "annot") as? AnnotationView {
return view
}
return AnnotationView(annotation: annotation, reuseIdentifier: "annot")
}

determine if MKMapView was dragged/moved

Is there a way to determine if a MKMapView was dragged around?
I want to get the center location every time a user drags the map using CLLocationCoordinate2D centre = [locationMap centerCoordinate]; but I'd need a delegate method or something that fires as soon as the user navigates around with the map.
The code in the accepted answer fires when the region is changed for any reason. To properly detect a map drag you have to add a UIPanGestureRecognizer. Btw, this is the drag gesture recognizer (panning = dragging).
Step 1: Add the gesture recognizer in viewDidLoad:
-(void) viewDidLoad {
[super viewDidLoad];
UIPanGestureRecognizer* panRec = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(didDragMap:)];
[panRec setDelegate:self];
[self.mapView addGestureRecognizer:panRec];
}
Step 2: Add the protocol UIGestureRecognizerDelegate to the view controller so it works as delegate.
#interface MapVC : UIViewController <UIGestureRecognizerDelegate, ...>
Step 3: And add the following code for the UIPanGestureRecognizer to work with the already existing gesture recognizers in MKMapView:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
Step 4: In case you want to call your method once instead 50 times per drag, detect that "drag ended" state in your selector:
- (void)didDragMap:(UIGestureRecognizer*)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded){
NSLog(#"drag ended");
}
}
This is the only way that worked for me that detects pan as well as zoom changes initiated by user:
- (BOOL)mapViewRegionDidChangeFromUserInteraction
{
UIView *view = self.mapView.subviews.firstObject;
// Look through gesture recognizers to determine whether this region change is from user interaction
for(UIGestureRecognizer *recognizer in view.gestureRecognizers) {
if(recognizer.state == UIGestureRecognizerStateBegan || recognizer.state == UIGestureRecognizerStateEnded) {
return YES;
}
}
return NO;
}
static BOOL mapChangedFromUserInteraction = NO;
- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
{
mapChangedFromUserInteraction = [self mapViewRegionDidChangeFromUserInteraction];
if (mapChangedFromUserInteraction) {
// user changed map region
}
}
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
if (mapChangedFromUserInteraction) {
// user changed map region
}
}
(Just the) Swift version of #mobi's excellent solution:
private var mapChangedFromUserInteraction = false
private func mapViewRegionDidChangeFromUserInteraction() -> Bool {
let view = self.mapView.subviews[0]
// Look through gesture recognizers to determine whether this region change is from user interaction
if let gestureRecognizers = view.gestureRecognizers {
for recognizer in gestureRecognizers {
if( recognizer.state == UIGestureRecognizerState.Began || recognizer.state == UIGestureRecognizerState.Ended ) {
return true
}
}
}
return false
}
func mapView(mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
mapChangedFromUserInteraction = mapViewRegionDidChangeFromUserInteraction()
if (mapChangedFromUserInteraction) {
// user changed map region
}
}
func mapView(mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
if (mapChangedFromUserInteraction) {
// user changed map region
}
}
Swift 3 solution to Jano's answer above:
Add the Protocol UIGestureRecognizerDelegate to your ViewController
class MyViewController: UIViewController, UIGestureRecognizerDelegate
Create the UIPanGestureRecognizer in viewDidLoad and set delegate to self
viewDidLoad() {
// add pan gesture to detect when the map moves
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.didDragMap(_:)))
// make your class the delegate of the pan gesture
panGesture.delegate = self
// add the gesture to the mapView
mapView.addGestureRecognizer(panGesture)
}
Add a Protocol method so your gesture recognizer will work with the existing MKMapView gestures
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
Add the method that will be called by the selector in your pan gesture
func didDragMap(_ sender: UIGestureRecognizer) {
if sender.state == .ended {
// do something here
}
}
Look at the MKMapViewDelegate reference.
Specifically, these methods may be useful:
- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
Make sure your map view's delegate property is set so those methods get called.
In my experience, similar to "search while typing", I found a timer is the most reliable solution. It removes the need for adding additional gesture recognizers for panning, pinching, rotating, tapping, double tapping, etc.
The solution is simple:
When the map region changes, set/reset the timer
When the timer fires, load markers for the new region
import MapKit
class MyViewController: MKMapViewDelegate {
#IBOutlet var mapView: MKMapView!
var mapRegionTimer: NSTimer?
// MARK: MapView delegate
func mapView(mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
setMapRegionTimer()
}
func setMapRegionTimer() {
mapRegionTimer?.invalidate()
// Configure delay as bet fits your application
mapRegionTimer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "mapRegionTimerFired:", userInfo: nil, repeats: false)
}
func mapRegionTimerFired(sender: AnyObject) {
// Load markers for current region:
// mapView.centerCoordinate or mapView.region
}
}
Another possible solution is to implement touchesMoved: (or touchesEnded:, etc.) in the view controller that holds your map view, like so:
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesMoved:touches withEvent:event];
for (UITouch * touch in touches) {
CGPoint loc = [touch locationInView:self.mapView];
if ([self.mapView pointInside:loc withEvent:event]) {
#do whatever you need to do
break;
}
}
}
This might be simpler than using gesture recognizers, in some cases.
A lot of these solutions are on the hacky / not what Swift intended side, so I opted for a cleaner solution.
I simply subclass MKMapView and override touchesMoved. While this snippet does not include it, I would recommend creating a delegate or notification to pass on whatever information you want regarding the movement.
import MapKit
class MapView: MKMapView {
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
print("Something moved")
}
}
You will need to update the class on your storyboard files to point to this subclass, as well as modify any maps you create through other means.
As noted in the comments, Apple discourages the use of subclassing MKMapView. While this falls to the discretion of the developer, this particular usage does not modify the behavior of the map & has worked for me without incident for over three years. However, past performance does not indicate future compatibility, so caveat emptor.
You can also add a gesture recognizer to your map in Interface Builder. Link it up to an outlet for its action in your viewController, I called mine "mapDrag"...
Then you'll do something like this in your viewController's .m:
- (IBAction)mapDrag:(UIPanGestureRecognizer *)sender {
if(sender.state == UIGestureRecognizerStateBegan){
NSLog(#"drag started");
}
}
Make sure you have this there too:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
Of course you'll have to make your viewController a UIGestureRecognizerDelegate in your .h file in order for that to work.
Otherwise the map's responder is the only one who hears the gesture event.
I know this is an old post but here my Swift 4/5 code of Jano's answer whit Pan and Pinch gestures.
class MapViewController: UIViewController, MapViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.didDragMap(_:)))
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(self.didPinchMap(_:)))
panGesture.delegate = self
pinchGesture.delegate = self
mapView.addGestureRecognizer(panGesture)
mapView.addGestureRecognizer(pinchGesture)
}
}
extension MapViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
#objc func didDragMap(_ sender: UIGestureRecognizer) {
if sender.state == .ended {
//code here
}
}
#objc func didPinchMap(_ sender: UIGestureRecognizer) {
if sender.state == .ended {
//code here
}
}
}
Enjoy!
To recognize when any gesture ended on the mapview:
[https://web.archive.org/web/20150215221143/http://b2cloud.com.au/tutorial/mkmapview-determining-whether-region-change-is-from-user-interaction/)
This is very useful for only performing a database query after the user is done zooming/rotating/dragging the map around.
For me, the regionDidChangeAnimated method only was called after the gesture was done, and didn't get called many times while dragging/zooming/rotating, but it's useful to know if it was due to a gesture or not.
You can check for animated property
if false then user dragged map
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
if animated == false {
//user dragged map
}
}
Jano's answer worked for me, so I thought I'd leave an updated version for Swift 4 / XCode 9 as I'm not particularly proficient in Objective C and I'm sure there are a few others that aren't either.
Step 1: Add this code in viewDidLoad:
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didDragMap(_:)))
panGesture.delegate = self
Step 2: Make sure your class conforms to the UIGestureRecognizerDelegate:
class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate, UIGestureRecognizerDelegate {
Step 3: Add the following function to make sure your panGesture will work simultaneously with other gestures:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
Step 4: And ensuring your method isn't called "50 times per drag" as Jano rightly points out:
#objc func didDragMap(_ gestureRecognizer: UIPanGestureRecognizer) {
if (gestureRecognizer.state == UIGestureRecognizerState.ended) {
redoSearchButton.isHidden = false
resetLocationButton.isHidden = false
}
}
*Note the addition of #objc in the last step. XCode will force this prefix on your function in order for it compile.
I was trying to have an annotation in the center of the map that is always at the center of the map no matter what the uses does. I tried several of the approaches mentioned above, and none of them were good enough. I eventually found a very simple way of solving this, borrowing from the Anna's answer and combining with Eneko's answer. It basically treats the regionWillChangeAnimated as the start of a drag, and regionDidChangeAnimated as the end of one, and uses a timer to update the pin in real-time:
var mapRegionTimer: Timer?
public func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
mapRegionTimer?.invalidate()
mapRegionTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true, block: { (t) in
self.myAnnotation.coordinate = CLLocationCoordinate2DMake(mapView.centerCoordinate.latitude, mapView.centerCoordinate.longitude);
self.myAnnotation.title = "Current location"
self.mapView.addAnnotation(self.myAnnotation)
})
}
public func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
mapRegionTimer?.invalidate()
}
enter code hereI managed to do implement this in easiest way, which handles all interaction with map (tapping/double/N tapping with 1/2/N fingers, pan with 1/2/N fingers, pinch and rotations
Create gesture recognizer and add to map view's container
Set gesture recognizer's delegate to some object implementing UIGestureRecognizerDelegate
Implement gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) method
private func setupGestureRecognizers()
{
let gestureRecognizer = UITapGestureRecognizer(target: nil, action: nil)
gestureRecognizer.delegate = self
self.addGestureRecognizer(gestureRecognizer)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool
{
self.delegate?.mapCollectionViewBackgroundTouched(self)
return false
}
this worked for me with swift 5
let manager = CLLocationManager()
#IBOutlet weak var map: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
manager.delegate = self
manager.requestWhenInUseAuthorization()
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.startUpdatingLocation()
map.delegate = self
}
extension ViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if mapView.userLocation.isEqual(annotation) {
return nil;
}
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "AnnotationView")
if annotationView == nil {
annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "AnnotationView")
}
annotationView?.isDraggable = true
annotationView?.canShowCallout = true
return annotationView
}
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, didChange newState: MKAnnotationView.DragState, fromOldState oldState: MKAnnotationView.DragState) {
switch newState {
case .starting:
print(".starting")
case .dragging:
print(".dragging")
case .ending:
print(".ending")
case .canceling:
print(".canceling")
case .none:
print(".none")
default: break
}
}
}
Updated 2023 version of the accepted answer, except it checks all mapView subviews and not just the first subview (to prevent future complications with OS updates). It also doesn't check for the .began state of the gesture, which I found unneeded for my use-case, so feel free to add it within the .contains(where:) block if your use-case requires.
/// Obtains all gestures within the mapView subviews to see if any
/// of the gestures changed state to began or ended.
/// - Returns: True if any gesture within the mapView changed state to ended, false otherwise.
private var regionDidChangeFromUserInteraction: Bool {
self.map.subviews // Look at all mapView subviews
.compactMap { $0.gestureRecognizers } // look at all subview gesture recognizers
.reduce([], +) // Reduces an Array of Arrays of gestures to an Array of gestures
.contains(where: { $0.state == .ended }) // if ended returns true else false
}
/// The framework calls this method at the beginning of a change to the map’s visible region.
func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
guard regionDidChangeFromUserInteraction else {
print("mapView:regionWillChangeAnimated - No Interaction detected.")
return
}
print("mapView:regionWillChangeAnimated - User Interaction detected.")
}
/// The map view calls this method at the end of a change to the map’s visible region.
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
guard regionDidChangeFromUserInteraction else {
print("mapView:regionDidChangeAnimated - No Interaction detected.")
return
}
print("mapView:regionDidChangeAnimated - User Interaction detected.")
}
First, make sure your current view controller is a delegate of the map. So set your Map View delegate to self and add MKMapViewDelegate to your view controller. Example below.
class Location_Popup_ViewController: UIViewController, MKMapViewDelegate {
// Your view controller stuff
}
And add this to your map view
var myMapView: MKMapView = MKMapView()
myMapView.delegate = self
Second, add this function which is fired when the map is moved. It will filter out any animations and only fire if interacted with.
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
if !animated {
// User must have dragged this, filters out all animations
// PUT YOUR CODE HERE
}
}

Detect Tap on CalloutBubble in MKAnnotationView

Im working with MKMapView and MKAnnotationView.
I have an annotation in the map. When the users tap on it, the callOut Bubble is displayed. When the annotation is tapped again ( and the callOut Bubble is visible ) i need to change to another view.
How can i detect the second tap, or the tap in the bubble?
Could you add a gesture recognizer when you're initializing the MKAnnotationView?
Here's the code for inside dequeueReusableAnnotationViewWithIdentifier:
UITapGestureRecognizer *tapGesture =
[[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(calloutTapped:)];
[theAnnotationView addGestureRecognizer:tapGesture];
[tapGesture release];
The method for the gesture recognizer:
-(void) calloutTapped:(id) sender {
// code to display whatever is required next.
// To get the annotation associated with the callout that caused this event:
// id<MKAnnotation> annotation = ((MKAnnotationView*)sender.view).annotation;
}
Here's the swift version of Dhanu's answer, including getting data from the item selected to pass to the next view controller:
func mapView(mapView: MKMapView, didSelectAnnotationView view: MKAnnotationView) {
let gesture = UITapGestureRecognizer(target: self, action: #selector(MyMapViewController.calloutTapped(_:)))
view.addGestureRecognizer(gesture)
}
func calloutTapped(sender:UITapGestureRecognizer) {
guard let annotation = (sender.view as? MKAnnotationView)?.annotation as? MyAnnotation else { return }
selectedLocation = annotation.myData
performSegueWithIdentifier("mySegueIdentifier", sender: self)
}
To tap the callout button after the user has clicked on the Annotation view, add a UITapGestureRecognizer in didSelectAnnotationView. This way you can implement tap on the callout without needing the accessory views.
You can then get the annotation object back from the sender for further action.
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(calloutTapped:)];
[view addGestureRecognizer:tapGesture];
}
-(void)calloutTapped:(UITapGestureRecognizer *) sender
{
NSLog(#"Callout was tapped");
MKAnnotationView *view = (MKAnnotationView*)sender.view;
id <MKAnnotation> annotation = [view annotation];
if ([annotation isKindOfClass:[MKPointAnnotation class]])
{
[self performSegueWithIdentifier:#"annotationDetailSegue" sender:annotation];
}
}
Try to set custom image for button without changing UIButtonTypeDetailDisclosure type.
UIButton *detailButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
[detailButton setImage:[UIImage imageNamed:#"icon"] forState:UIControlStateNormal];
Here is my solution to this question:
Swift 5
First, add MKMapViewDelegate method
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView)
This gets called when annotation is selected and the callout bubble is shown. You can extract the callout bubble view like this, and add the tap gesture recognizer to it, like so:
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
// Set action to callout view
guard let calloutView = view.subviews.first else { return }
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(userDidTapAnnotationCalloutView(_:)))
calloutView.addGestureRecognizer(tapGesture)
}
And handle tap action like this:
#objc
private func userDidTapAnnotationCalloutView(_ sender: UITapGestureRecognizer) {
// Handle tap on callout here
}
This works in Swift 5.2
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
let gesture = UITapGestureRecognizer(target: self, action: #selector(calloutTapped))
view.addGestureRecognizer(gesture)
}
#objc func calloutTapped() {
print ("Callout Tapped")
}
Swift 3, using On. You need to handle rightCalloutAccessoryView
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
switch annotation {
case let annotation as Annotation:
let view: AnnotationView = mapView.dequeue(annotation: annotation)
view.canShowCallout = true
let button = UIButton(type: .detailDisclosure)
button.on.tap { [weak self] in
self?.handleTap(annotation: annotation)
}
view.rightCalloutAccessoryView = button
return view
default:
return nil
}
}

Resources