Tapping an MKAnnotation to "select" it is REALLY slow - ios

There's almost a 0.5 second delay between tapping and when the callout is shown for an annotation on an MKMapView.
Does anyone know why this is the case, and how I can make it instantaneously responsive when a user taps on the map?
This happens even with the user location annotation that displays "Current Location" in a callout when tapped. I want it to display that instantly when tapped, no weird delay.
EDIT: I think it's due to the setSelected function that didSelectAnnotationView calls. setSelected has an 'animated' property that might be slowing it. How do I eliminate that animation?

after doing a lot of research I found a solution for this! It's a tiny tiny bit hacky, but it works like a charm.
The secret is, that when turning off zoom for the map, the didSelect listener triggers immediately. As we need zoom (of course), what we need to do is, to temporarily disable the zoom, just for the moment of the didSelect event!
In Swift:
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
gestureRecognizer.numberOfTapsRequired = 1
gestureRecognizer.numberOfTouchesRequired = 1
gestureRecognizer.delegate = self
mapView.addGestureRecognizer(gestureRecognizer)
and
#objc func handleTap(_ sender: UITapGestureRecognizer? = nil) {
// disabling zoom, so the didSelect triggers immediately
mapView.isZoomEnabled = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.mapView.isZoomEnabled = true
}
}
This gesture event triggers before the didSelect event. So the moment the didSelect events is called, zoom is turned off and it does trigger without delay!
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
mapView.isZoomEnabled = true // Not really necessary
// Triggered immediately, do something
}
Note: This disables doubleTap gestures for the map, but I guess they are not used too much. So if you want a quick response, you need to live with it!

Unfortunately, there's nothing you can do about this. It's for the exact same reason that tapping links in Mobile Safari is slow: the gesture recognizers have to jostle for a while to decide whether you might be scrolling (dragging) before they agree that you are tapping.
So, it has nothing to do with the animation. It's just the nature of gesture recognition in this situation.

My solution is to enable zooming on map and add individual tap handler on MKAnnotationView subclass.
Native zoom MKOneHandedZoomGestureRecognizer, MKZoomingPanGestureRecognizer and MKConditionalPanZoomGestureRecognizer will work.
But also immediate reaction on tap will be handled by button or tap recognizer on annotation view.

#tobidude's answer works, but it can be slightly improved.
Instead of adding the TapGestureRecognizer to the mapview, add it your AnnotationView subclass on initialization. This way, you don't have to ignore all double taps on the mapview.
In Swift 5:
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
clusteringIdentifier = MKMapViewDefaultClusterAnnotationViewReuseIdentifier
setupGestureRecognizerToPreventInteractionDelay()
}
Then:
private func setupGestureRecognizerToPreventInteractionDelay() {
let quickSelectGestureRecognizer = UITapGestureRecognizer()
quickSelectGestureRecognizer.delaysTouchesBegan = false
quickSelectGestureRecognizer.delaysTouchesEnded = false
quickSelectGestureRecognizer.numberOfTapsRequired = 1
quickSelectGestureRecognizer.numberOfTouchesRequired = 1
quickSelectGestureRecognizer.delegate = self
self.addGestureRecognizer(quickSelectGestureRecognizer)
}
Lastly:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
mapView?.isZoomEnabled = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.mapView?.isZoomEnabled = true
}
return false
}

With this code, you can make it work fairly easy.
Add a new file and copy paste this:
import Foundation
import MapKit
class MKAnnotationViewWithoutSlowTap: MKAnnotationView, UIGestureRecognizerDelegate {
unowned let map: MKMapView
init(annotation: MKAnnotation, reuseIdentifier: String, map: MKMapView) {
self.map = map
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
setupGestureRecognizerToPreventInteractionDelay()
}
required init?(coder: NSCoder) {
fatalError("Not used")
}
private func setupGestureRecognizerToPreventInteractionDelay() {
let quickSelectGestureRecognizer = UITapGestureRecognizer()
quickSelectGestureRecognizer.delaysTouchesBegan = false
quickSelectGestureRecognizer.delaysTouchesEnded = false
quickSelectGestureRecognizer.numberOfTapsRequired = 1
quickSelectGestureRecognizer.numberOfTouchesRequired = 1
quickSelectGestureRecognizer.delegate = self
addGestureRecognizer(quickSelectGestureRecognizer)
}
#objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
map.isZoomEnabled = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.map.isZoomEnabled = true
}
return false
}
}
Next, change your calls from the initializer for MKAnnotationView to MKAnnotationViewWithoutSlowTap

Related

Customize long press gesture recognizer

I want to customize my long press gesture recognizer in the following ways:
1) When I hold down on an object for 0.5 seconds, the object darkens, and
2) When I continue to hold down on the object for another second (total of 1.5 seconds), some action happens (e.g. the object disappears).
Essentially, by holding down on an object for a minimum of 1.5 seconds, two actions happened at two separate times. I also have a tap gesture recognizer, which might affect things.
The answer from #nathan is essentially fine but a detail is missing you need to implement the UIGestureRecognizerDelegate to allow both gestures works simultaneously, so this is my code
class ViewController: UIViewController, UIGestureRecognizerDelegate{
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
//this for .5 time
let firstGesture = UILongPressGestureRecognizer(target: self, action: #selector(firstMethod))
//this for 1.5
let secondGesture = UILongPressGestureRecognizer(target: self, action: #selector(secondMethod))
secondGesture.minimumPressDuration = 1.5
firstGesture.delegate = self
secondGesture.delegate = self
self.view.addGestureRecognizer(firstGesture)
self.view.addGestureRecognizer(secondGesture)
}
func firstMethod() {
debugPrint("short")
}
func secondMethod() {
debugPrint("long")
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool{
return true
}
}
Hope this help
See Reinier's solution as it's the correct one. This one adds a delay to satisfy require(toFail:)
You can set the timing using the property minimumPressDuration (in seconds, default is 0.5)
let quickActionPress = UILongPressGestureRecognizer(target: self, action: #selector(ViewController.zeroFiveSecondPress(gesture:))) // 0.5 seconds by default
let laterActionPress = UILongPressGestureRecognizer(target: self, action: #selector(ViewController.oneFiveSecondPress(gesture:)))
laterActionPress.minimumPressDuration = 1.5
someView.addGestureRecognizer(quickActionPress)
someView.addGestureRecognizer(laterActionPress)
// If 1.5 detected, only execute that one
quickActionPress.require(toFail: laterActionPress)
#objc func zeroFiveSecondPress(gesture: UIGestureRecognizer) {
// Do something
print("0.5 press")
}
#objc func oneFiveSecondPress(gesture: UIGestureRecognizer) {
zeroFiveSecondPress(gesture: gesture)
// Do something else
print("1.5 press")
}

GestureRecognizer taking all Touch Input

I have this Setup in my Storyboard.
In my first ViewController Scene I have a MapView from MapBox. In there I have put a TextField (AddressTextField). On that TextField when touching the view, i'm running self.addressTextField.resignFirstResponder(), but after that neither the mapview, nor any other element in there or in the Embedded Segues react on a touch or click. Probably this is because I didn't completely understand the system of the First Responder. I'm thankful for every help.
Edit 1:
I think I know what's going on now, but I don't know how to fix it. When I add the Gesture Recognizer to the View (or to the mapView, that doesn't matter), the other UIViews and the MapView do not recognize my Tap-Gestures anymore. When I am not adding the Recognizer everything works fine. It seems as if the Gesture Recognizer is recognizing every tap I make on either the UIViews or the MapView and therefore other gestures are not recognized.
Edit 2:
I just added a print() to dismissKeyboard(). As soon as any Touch Event gets recognized on the MapView or the other UIViews, dismissKeyboard() gets called. So I think my thought of Edit 1 was correct. Does anyone know how I can solve this, so that it's not only dismissKeyboard() that gets called ?
Some Code:
func dismissKeyboard(){
self.addressTextField.resignFirstResponder()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
dismissKeyboard()
return true
}
//Class (only partially)
class ViewController: UIViewController, MGLMapViewDelegate, CLLocationManagerDelegate, UITextFieldDelegate {
override func viewDidLoad(){
mapView.delegate = self
addressTextField.delegate = self
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
self.mapView.addGestureRecognizer(tap)
}
}
Others are just #IBActions linked to the Buttons, or other elements.
try this:
func dismissKeyboard(){
view.endEditing(true)
}
hope it helps!
After I knew the real issue I was able to solve the problem. I declared a var keyboardEnabled. Then I added these lines to my class.
class ViewController: UIViewController, UIGestureRecognizerDelegate {
var keyboardEnabled = false
override func viewDidLoad(){
super.viewDidLoad()
//Looks for single tap
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
self.mapView.addGestureRecognizer(tap)
}
/* Setting keyboardEnabled */
//Editing Target did end
#IBAction func editingTargetDidEnd(_ sender: Any) {
keyboardEnabled = false
}
//Editing TextField Started
#IBAction func editingAdressBegin(_ sender: Any) {
keyboardEnabled = true
}
//Call this function when the tap is recognized.
func dismissKeyboard() {
self.mapView.endEditing(true)
keyboardEnabled = false
}
//Implementing the delegate method, so that I can add a statement
//decide when the gesture should be recognized or not
//Delegate Method of UITapGestureRecognizer
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
return keyboardEnabled
}
}
With this solution keyboardEnabled takes care of deciding wether my UIGestureRecognizer should react or not. If the Recognizer doesn't react, the Gesture is simply passed on to the UIViews or other Elements that are in my MapView.
Thanks for all your answers!

Swift 3 UITapGestureRecognizer not receiving tap

I want to add a gesture recognizer to one of my views to detect taps,
here is my code:
class DateTimeContainer: UIView, UIGestureRecognizerDelegate {
override func awakeFromNib() {
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.onTap))
gesture.delegate = self
self.addGestureRecognizer(gesture)
}
func onTap(_ gestureRecognizer: UITapGestureRecognizer) {
openDatePicker()
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if touch.view?.tag != self.datePickerTag && !self.isDatePickerOpen() {
return true
}
return false
}
}
When I tap on my view the code enters into the gestureRecognizer shouldReceive method and enters the condition for which it returns true.
But the onTap method is never called, can someone tell me why?
EDIT
Adding self.isUserInteractionEnabled = true I managed to get it working.
But it had a strange behaviour: it was like it received the tap only in the main view and not in subviews.
So to make it simple I solved by adding a button inside my view and by using:
self.button?.addTarget(self, action: #selector(onTap), for: .touchUpInside)
You may have forgotten to do this:
self.isUserInteractionEnabled = true
Also, typically the UIViewController subclass is usually the target and contains the method used for the tap gesture.
You don't need to set a delegate for a tap gesture (usually)
You can You Gesture
let recognizer = UITapGestureRecognizer(target: self,action:#selector(self.handleTap(recognizer:)))
userImage.isUserInteractionEnabled = true
recognizer.delegate = self as? UIGestureRecognizerDelegate
userImage.addGestureRecognizer(recognizer)
And method call selector
func handleTap(recognizer:UITapGestureRecognizer) {
// do your coding here on method
}
Try Writing the codes
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.onTap))
gesture.delegate = self
self.addGestureRecognizer(gesture)
inside init block not in awakeFromNib()
I noticed that you are adding your gesture on a UIView. As such, at that ViewController where you have added this ContainerView, inside the viewDidLoad method, add this line of code.
override func viewDidLoad() {
super.viewDidLoad()
self.YourContainerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(YourCurrentViewController.openDatePicker())))
self.YourContainerView.isUserInteractionEnabled = true
}
Note. Do not forget to add isUserInteractionEnabled to true.

UITapGestureRecogniser in GMSMapView

I am creating an app using swift. In one of my ViewController, I have a GMSMapView that I create programatically. I want user to have the capability triggering an action when clicking on the map.
What I have done :
import UIKit
class MapViewController: UIViewController, GMSMapViewDelegate {
let mapView = GMSMapView()
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
mapView.settings.scrollGestures = false
mapView.frame = CGRectMake(0, 65, 375, 555)
view.addSubview(mapView)
var tap = UITapGestureRecognizer(target: self, action: "tap:")
mapView.addGestureRecognizer(tap)
}
func tap(recogniser:UITapGestureRecognizer)->Void{
println("it works")
}
}
I have tried to override touchesBegan, didnt work. I have tried to insert mapView.userInteractionEnabled = true, didnt work...
Any idea?
I managed to do it with
func mapView(mapView: GMSMapView!, didTapAtCoordinate coordinate: CLLocationCoordinate2D) {
println("It works")
}
But if someone could explain to me why the other solution didn't work, it would be great!
You can use default MapVIew LongPress event
/**
* Called after a long-press gesture at a particular coordinate.
*
* #param mapView The map view that was pressed.
* #param coordinate The location that was pressed.
*/
- (void)mapView:(GMSMapView *)mapView
didLongPressAtCoordinate:(CLLocationCoordinate2D)coordinate;
The map view already has its own gesture recognizers for panning, zooming etc.
So you probably need to tell the system that it should take care on multiple gesture recognizers.
As part of the UIGestureRecognizerDelegate protocol:
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}

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

Resources