determine if MKMapView was dragged/moved

IosMkmapview

Ios Problem Overview


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.

Thanks in advance

Ios Solutions


Solution 1 - Ios

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

Solution 2 - Ios

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

Solution 3 - Ios

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

Solution 4 - Ios

Look at the [MKMapViewDelegate][1] 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. [1]: http://developer.apple.com/library/ios/#documentation/MapKit/Reference/MKMapViewDelegate_Protocol/MKMapViewDelegate/MKMapViewDelegate.html

Solution 5 - Ios

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

Solution 6 - Ios

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:

  1. When the map region changes, set/reset the timer

  2. 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
         }
    
     }
    

Solution 7 - Ios

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.

Solution 8 - Ios

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.

Solution 9 - Ios

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.

Solution 10 - Ios

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.

Solution 11 - Ios

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!

Solution 12 - Ios

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

Solution 13 - Ios

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.

Solution 14 - Ios

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()
}

Solution 15 - Ios

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

  1. Create gesture recognizer and add to map view's container
  2. Set gesture recognizer's delegate to some object implementing UIGestureRecognizerDelegate
  3. 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 }

Solution 16 - Ios

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionhgbnerdView Question on Stackoverflow
Solution 1 - IosJanoView Answer on Stackoverflow
Solution 2 - IosSnowmanView Answer on Stackoverflow
Solution 3 - IoshEADcRASHView Answer on Stackoverflow
Solution 4 - Iosuser467105View Answer on Stackoverflow
Solution 5 - Iosdst3pView Answer on Stackoverflow
Solution 6 - IosEneko AlonsoView Answer on Stackoverflow
Solution 7 - IosAaronView Answer on Stackoverflow
Solution 8 - IosCodeBenderView Answer on Stackoverflow
Solution 9 - IosCommaToastView Answer on Stackoverflow
Solution 10 - IosDoug VossView Answer on Stackoverflow
Solution 11 - IosBossOzView Answer on Stackoverflow
Solution 12 - IosRomaView Answer on Stackoverflow
Solution 13 - IosPigpocketView Answer on Stackoverflow
Solution 14 - IosGadzairView Answer on Stackoverflow
Solution 15 - IosNikita IvaniushchenkoView Answer on Stackoverflow
Solution 16 - IosLiam BollingView Answer on Stackoverflow