I have an app that implements a MKMapView where I want to track and follow the user location, and I kind of managed to make that work, the only problem is that even when tracking is set to .none the map returns to the user location sometimes.
First of all, here's a GIF of it happening (uploaded to imgur because the file was too large for SO): http://i.imgur.com/2FoQCoi.gifv
The icon on the lower right is supposed to indicate if the map is tracking the user location, on the GIF it's grey because it's supposed to be off.
The code for the button is:
var isFollowing: Bool {
get {
return chkFollow.checked
}
set {
chkFollow.checked = newValue
if newValue {
mapView.setUserTrackingMode(.follow, animated: true)
print("did set to follow")
}
else {
mapView.setUserTrackingMode(.none, animated: true)
print("did set to none")
}
}
}
I also set up the mapView(_, regionWillChangeAnimated) method to be the following:
func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
isFollowing = false
}
and
func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
mapView.userTrackingMode = .none
}
Neither worked correctly.
When I click the button the print statement works and prints "did set to none" on the console correctly so I know that the method is being called, just for some reason it still follows the user.
If there's any other info that helps, I'll gladly send it. chkFollow is a custom checkbox control, I don't think it's code is relevant here.
Thanks in advance!
Sorry everybody, I got it when I was recreating it in a sample project.
The problem was, I wanted the app track the user's position on start up, so I set UserTrackingMode to .follow inside mapViewDidFinishRenderingMap. So when the map moved enough to re-render everything that was being called and the map started tracking the user back again.
To solve it I added a bool variable (isFirstLoad) and only set up UserTrackingMode when the user first loaded the app. Thanks for the comments and sorry to take your time with this :)
Related
I have the following function in an extension of MKMapView that let me reload a specific annotation and keep it selected if needed and the issue is every time the self.removeAnnotation is called, the whole map is reloaded (or at least all the pins "jumped" as if a reload occurred)
Is there a way to achieve a reload of a single annotation without having the visual of a whole map reloading ?
func reloadAnnotation(_ annotation: MKAnnotation, keepSelection: Bool = true) {
let isSelected = selectedAnnotations.contains(where: annotation.isEqual(_:))
//already tried **UIView.performWithoutAnimation** which decrease the jumping effect
// UIView.performWithoutAnimation {
self.removeAnnotation(annotation)
self.addAnnotation(annotation)
// }
guard isSelected && keepSelection else {
return
}
self.selectedAnnotations.append(annotation)
}
In my experience the unwanted "whole map reloading" visual effect comes from the recalculation of clusters which is triggered by self.removeAnnotation(annotation) and self.addAnnotation(annotation).
So you have to avoid this methods if you just want to update some visual information.
My assumption is that your callout and/or title changed dynamically while selected and you reload because you want to render the changed callout information.
func reloadAnnotation(_ annotation: MKAnnotation, keepSelection: Bool = true) {
let annotationView = mapView.view(for: annotation) as? MyCoolAnnotationView
if let annotationView {
// do stuff like
annotationView.setNeedsLayout()
}
}
annotationView will be nil if the annotation is not in the visible region or part of a cluster. In that case you don't want to reload anyways.
Instead of annotationView.setNeedsLayout() you might call your own method that does whatever you want.
I'm using the default Google Map recenter button which is shown by default. How do I ensure that the recenter button is not shown by default and it's only shown when someone moves the map using Swift?
func SetupMap() {
googleMapView.settings.myLocationButton = true
}
That's pretty easy. You know how to toggle the visibility of the myLocationButton, so what's left is to think harder.
If you take time to review the GoogleMap's GMSMapViewDelegate, you will see that there are couple of methods/functions that will allow you to further your idea.
So set your mapView's delegate to your class (controller), and conform to that GMSMapViewDelegate protocol, and then implement those methods.
willMove
didChange position
These are all you need.
The willMove gets invoked when you start dragging the mapView. On the other hand, the didChange position gets called when the camera did change.
If you do these things, you'll get even nearer towards your goal. However, you might need some debounce feature in your code, because you only want to hide the location button just once, just after the user stops dragging the camera.
var debounce_timer: Timer?
extension MapsViewController: GMSMapViewDelegate {
func mapView(_ mapView: GMSMapView, willMove gesture: Bool) {
print("GMS: will move")
mapView.settings.myLocationButton = true
}
func mapView(_ mapView: GMSMapView, didChange position: GMSCameraPosition) {
print("GMS: didChane camera position")
debounce_timer?.invalidate()
debounce_timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in
mapView.settings.myLocationButton = false
}
}
}
Voila!
Demo:
I'm trying to call the mapView(mapView: MKMapView, didDeselectAnnotationView view: MKAnnotationView) function programmatically using this code:
dispatch_async(dispatch_get_main_queue()) {
for item in self.largeMapView.selectedAnnotations {
self.largeMapView.deselectAnnotation(item, animated: false)
}
}
but this doesn't do anything. If I press on the map, it is called, but I want it to be called without the need to press.
Not exactly sure why, but this code in full seems to solve the problem :
dispatch_async(dispatch_get_main_queue()) {
for item in self.largeMapView.selectedAnnotations {
self.largeMapView.deselectAnnotation(item, animated: false)
}
myView.pinTintColor = UIColor.greenColor()
calloutView.hidden = true
}
Manually changing the pinTintColor to the "deselected" one, and manually hiding the calloutView solved the problem.
I'm trying out mapbox (using the ios sdk) and I've run into a problem that I think I've narrowed down pretty far. This is my code:
func centerMap(location: CLLocationCoordinate2D) {
map.setCenterCoordinate(location,
zoomLevel: 14,
animated: true)
}
func mapView(mapView: MGLMapView, didDeselectAnnotation annotation: MGLAnnotation) {
dealDetails.hidden = false
}
func mapView(mapView: MGLMapView, didUpdateUserLocation userLocation: MGLUserLocation?) {
if let currentLocation = userLocation?.coordinate {
centerMap(currentLocation)
}
}
If I don't re-center the map when the user's location is updated (i.e., just commenting out the centerMap(currentLocation) call) then the annotation remains selected. Re-centering the map calls the didDeselectAnnotation function, and I can't figure out how to keep that annotation selected. Any help is appreciated!
I don't think there's any way around that if you update the center coordinate. You'd have to re-select the annotation. However, you probably don't need to do that. If you set the userTrackingMode on the map view to .Follow, it should re-center automatically.
I am working with a MKMapView that is placed on the third VC (MapViewController: UIViewController) in a tabbed application controller (AppTabController: UITabViewController). If I understand UITabViewController correctly, the MapViewController will not be instantiated until the user navigates to it.
The MKMapView is linked to the MapViewController via the outlet
#IBOutlet weak var mapView: MKMapView!
MapViewController conforms to the MKMapViewDelegate protocol through an extension to MapViewController and I’ve set the MKMapView delegate in the viewDidLoad method of MapViewController.
extension MapViewController: MKMapViewDelegate {
// delegate methods
}
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
}
In viewDidLoad of MapViewController I call showMyLocation() to centre the map on the user’s location in a 1 km. by 1 km. region and show their annotation.
The same method is linked to a “User” button on MapViewController’s nav bar which allows the user to return to their location if they want to.
#IBAction func showMyLocation() {
let region = MKCoordinateRegionMakeWithDistance(mapView.userLocation.coordinate, 1000, 1000)
mapView.setRegion(mapView.regionThatFits(region), animated: true)
}
I am not using a CLLocationManager to determine the user’s location. Because MKMapView is able to return the user’s location to the app.
Answers I've found so far point towards a CLLocationManager approach.
I am a little unsure about what appears to be a timing dependency in obtaining the user’s coordinates from MKMapView.
If I start the app and navigate immediately to the MapViewController tab the coordinates returned by mapView.userLocation.coordinate are 0,0
However if I start the app and wait for a minute or so before going to the MapViewController tab the coordinates are the expected ones for my location.
Finally, if I start the app and navigate immediately to the MapViewController the coordinates are 0,0 as above but if I immediately tap the “User” button the coordinates immediately update to the expected ones without the delay.
My solution so far is to implement the didUpdateUserLocation delegate method to tell me when MKMapView has obtained a valid location for the user.
func mapView(mapView: MKMapView, didUpdateUserLocation userLocation: MKUserLocation) {
if self.rideLocations.isEmpty {
self.showUserLocation()
}
}
My question is have I implemented the necessary pattern to deal with MKMapView user location behaviour timing?
My objective is to have the user’s location properly displayed on the MKMapView regardless of when they navigate to the tab that has the map on it but to avoid overuse of the device location services.
Thanks for all the answers so far - they’ve been a massive help in my Swift / IOS journey!
I know this is an old thread, but I thought I would add my two cents in case it helps someone else. To solve this problem you can implement the mapView delegate method mapView(_:viewFor:) which gets called every time an annotation is added to the map. This includes the user location. Just add the code at the top of the method. It's only called once when the MKUserLocation annotation is added to the mapView. In viewDidLoad disable the button.
`func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?{
if annotation is MKUserLocation {
//return nil so map view draws "blue dot" for standard user location
showMyLocation()
enable your button here.
return nil
}
//other code here.`