Swift Mapkit - Move Away From User Location - ios

I want to display user location and the surrounding area, but I also want to allow the user to pan around the area. Right now if I try to scroll somewhere else on the map it automatically takes me back to the base region with the user at the center. How do I stop this? I want to show the initial view with the user in the center, but I want to be able to scroll around too. Thanks in advance you guys are so helpful!
import UIKit
import MapKit
import CoreLocation
class ViewControllerMain: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
#IBOutlet weak var mapView: MKMapView!
var locationManager:CLLocationManager!
override func viewDidLoad() {
super.viewDidLoad()
locationManager = CLLocationManager()
locationManager.requestWhenInUseAuthorization()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.delegate = self
locationManager.startUpdatingLocation()
mapView.showsUserLocation = true
mapView.delegate = self
let longPress = UILongPressGestureRecognizer(target: self, action: "action:")
longPress.minimumPressDuration = 1.0
mapView.addGestureRecognizer(longPress)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
let regionToZoom = MKCoordinateRegionMake(manager.location.coordinate, MKCoordinateSpanMake(0.01, 0.01))
mapView.setRegion(regionToZoom, animated: true)
}

Your code in didUpdateLocations is resetting the region. You have two options.
Store in an ivar whether or not you have already set the first location. Only if you haven't do you then set the region.
Set a timer that runs for 15 seconds. If the map is moved by the user you reset the timer. When the timer expires you can recenter to the users location.
This will keep the map centred around the user but will enable them to pan around a bit to get some context.
This answer shows how to do it in Objective-C

locationManager.stopUpdatingLocation();
this is all you need at end of locationManager fun, if you are still looking for

This thread had a good example in both Swift and Obj C. Be sure to look for the comment on the answer I've linked to if you use Swift.
After you set that up, use Control Flow within the didUpdateLocations, so that it re-centers the user's location only if the user has not touched the map.
Here is my full code as an example:
#IBOutlet weak var theMap: MKMapView!
// ...
// This var and the three following functions are used to tell if the map moves because of the user.
// This is used in the control flow in didUpdateLocations
private var mapChangedFromUserInteraction = false
private func mapViewRegionDidChangeFromUserInteraction() -> Bool {
let view: UIView = self.theMap.subviews[0] as UIView
// 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
println("user changed map region")
}
}
func mapView(mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
if (mapChangedFromUserInteraction) {
// user changed map region
println("user changed map region")
}
}
// This function is called each time the user moves.
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
// Use Control Flow: if the user has moved the map, then don't re-center.
// NOTE: this is using 'mapChangedFromUserInteraction' from above.
if mapChangedFromUserInteraction == true {
// do nothing, because the user has moved the map.
}
else {
// update on location to re-center on the user.
// set X and Y distances for the span (zoom). This is very zoomed in.
let spanX = 0.0005
let spanY = 0.0005
// Create a region using the user's location, and the zoo.
var newRegion = MKCoordinateRegion(center: theMap.userLocation.coordinate, span: MKCoordinateSpanMake(spanX, spanY))
// set the map to the new region
theMap.setRegion(newRegion, animated: true)
}
}

Related

center the map on user's location didn't show the blue dot

I have a tab bar, one is used to display the map. When I switch to this tab bar, it will request permission and get the current location automatically.
I finished the code, set the simulated location to Apple, it will show a blue dot in the center of the screen, but when I chose the custom location, specify a location, the blue dot is not displayed in the center of the map.
I need to drag the map, and will find the blue dot near the center location about 1~2km. I tested on the real device and have the same problem.
use the method to show the current location auto, but not show the blue dot.
override func viewDidLoad() {
super.viewDidLoad()
locationManager = CLLocationManager()
locationManager!.delegate = self
if CLLocationManager.authorizationStatus() == .authorizedWhenInUse{
locationManager!.startUpdatingLocation()
locationManager!.desiredAccuracy = kCLLocationAccuracyBestForNavigation
}else{
locationManager!.requestWhenInUseAuthorization()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let newLocation = locations.last!
let coordinateRegion = MKCoordinateRegionMakeWithDistance(newLocation.coordinate, 100, 100)
mapView.setRegion(mapView.regionThatFits(coordinateRegion), animated: true)
location = newLocation
locationManager?.stopUpdatingLocation()
locationManager = nil
}
I added a button - when it not show the blue dot, clicking the button will set the blue dot in the center of the map. The two methods have the same function, but they produce different views. Why?
#IBAction func showUser() {
let region = MKCoordinateRegionMakeWithDistance(mapView.userLocation.coordinate, 100, 100)
mapView.setRegion(mapView.regionThatFits(region), animated: true)
}
Screenshot
I just use this coding to update to location with center of mapview
Please add this keys into info.plist file
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>NSLocationAlwaysUsageDescription</key>
<string></string>
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
locationManager.delegate = self
map.delegate = self
map.showsUserLocation = true
if CLLocationManager.authorizationStatus() == .authorizedWhenInUse{
locationManager.startUpdatingLocation()
locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
}else{
locationManager.requestWhenInUseAuthorization()
}
}
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
mapView .setCenter(userLocation.coordinate, animated: true)
}
You can download demo from HERE

update center of screen with current location

I have successfully made an app that shows a blinking blue dot where the user currently is. However as of right now the center of the screen does not follow the blue dot as the user moves, so if the user moves the blue dot just exits the screen and the user would have to scroll to keep up with it. That isnt user friendly! I have the below code:
import UIKit
import MapKit
import CoreLocation
class ViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
#IBOutlet weak var map: MKMapView!
let locationManager = CLLocationManager()
var mapp = MKMapView.self
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
if (CLLocationManager.locationServicesEnabled())
{
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
self.map.showsUserLocation = true
}
else
{
print("Location services are not enabled")
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
{
let location = locations.last
let center = CLLocationCoordinate2D(latitude: location!.coordinate.latitude, longitude: location!.coordinate.longitude)
let region = MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: 0.002, longitudeDelta: 0.002))
self.map.setRegion(region, animated: true)
self.locationManager.stopUpdatingLocation()
}
}
My only thoughts are that I could get rid of the "stopUpdatingLocation() section and maybe then it would continuously update the region with the new center for every new location, however I am not sure if not having the stopUpdatingLocation is bad practice? Thanks for all advice!
Use userTrackningMode which causes the map view to center the map on that location and begin tracking the user’s location. If the map is zoomed out, the map view automatically zooms in on the user’s location, effectively changing the current visible region.
To use it
mapView.setUserTrackingMode(.follow, animated:true)

How do i enabled a Google Maps button in a cell?

In a custom tableViewCell , I connect a View which is a google map class to the custom class
However it seems that these line of code doesn't run in the custom cell that I created
This cell is part of a tableView, I tried to instantiate this cell for example var gmapsCell = GMapTableViewCell in the viewDidLoad of the TableView but it seems it will always returns nil
So I change my approach to below codes
import UIKit
import GoogleMaps
class GMapTableViewCell: UITableViewCell, GMSMapViewDelegate {
let locationManager = CLLocationManager()
#IBOutlet var mapView: GMSMapView!
override func awakeFromNib() {
super.awakeFromNib()
locationManager.requestAlwaysAuthorization()
// For use in foreground
locationManager.requestWhenInUseAuthorization()
mapView.delegate = self
mapView.settings.myLocationButton = true
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
extension GMapTableViewCell: CLLocationManagerDelegate {
func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
if status == .AuthorizedWhenInUse {
locationManager.startUpdatingLocation()
mapView.myLocationEnabled = true
mapView.settings.myLocationButton = true
}
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
mapView.camera = GMSCameraPosition(target: location.coordinate, zoom: 15, bearing: 0, viewingAngle: 0)
locationManager.stopUpdatingLocation()
}
}
}
The problem right now is that
override func awakeFromNib() {
super.awakeFromNib()
locationManager.requestAlwaysAuthorization()
// For use in foreground
locationManager.requestWhenInUseAuthorization()
mapView.delegate = self
mapView.settings.myLocationButton = true
}
This will never run. My goal is simply to get to the device's current location. Are there any equivalent of viewDidLoad for custom cell?
You need to add "NSLocationWhenInUseUsageDescription" key as a string type in info.plist for the location Manager to start updating locations.
All that code inside awakeNib() should be written in tableView:cellForRowAtIndexPath.
Set all the delegates there.
It will work fine then.

How should I find the correct location data using userLocation in Mapbox for iOS SDK? (Swift)

I'm trying to find the latitude and longitude of the user's location so that I can center the map on the user in viewdidload.
I've implemented what seems to be the right code but the values of userLat (latitude) and userLon (longitude) are way off.
N.B. Somebody else had the same problem as me but his answer was never resolved:
Mapbox iOS8 Swift mapView.showUsersLocation
import Mapbox
class ViewController: UIViewController, MGLMapViewDelegate, CLLocationManagerDelegate {
// let locationManager = CLLocationManager()
#IBOutlet weak var mapView: MGLMapView!
override func viewDidLoad() {
super.viewDidLoad()
// Initalise map's center coordinate as vancouver
mapView.setCenterCoordinate(CLLocationCoordinate2D(latitude: 49.283382,
longitude: -123.117394),
zoomLevel: 15, animated: false)
view.addSubview(mapView)
// Set the delegate property of our map view to self after instantiating it.
mapView.delegate = self
// User location
mapView.showsUserLocation = true
let userLoc = mapView.userLocation!
userLoc.title = "Hello"
userLoc.subtitle = "I am here!"
let userLat = userLoc.coordinate.latitude
let userLon = userLoc.coordinate.longitude
print(userLat, userLon)
/*
self.locationManager.requestAlwaysAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.startUpdatingLocation()
}*/
}
func mapView(mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
return true
}
}
Resulting print:
3.40282346638529e+38 3.40282346638529e+38
The strange thing is that the annotation works fine, and when I click my location I get the title and subtitle.
The easiest way to center the map on the user's location is to set MGLMapView.userTrackingMode = .follow (MGLUserTrackingModeFollow in Objective C). This will automatically move the map when a location is available.
The reason why you're seeing bogus numbers for MGLMapView.userLocation is that the user's location typically isn't available yet in viewDidLoad. Use the mapView:didUpdateUserLocation: delegate method to be notified when the user's location becomes available and when it updates.
There is a delegate method called mapViewDidFinishLoadingMap. Set the center of the map to the user coordinates in this method.
func mapViewDidFinishLoadingMap(_ mapView: MGLMapView) {
mapView.setCenter((mapView.userLocation?.coordinate)!, animated: false)
}

How to detect the mapView was moved in Swift and update zoom

I'm trying to set a minimum zoom level on my map in Swift 2. I can't find any documentation on how to restrict a map from being zoomed too far in. What I've decided to try is to monitor for map movement (such as drag or zoom) and then set MKZoomScaleback to a minimum.
Most of the answers I've found for regionDidChangeAnimated are in Objective C, which I don't know and I'm having trouble converting them to Swift.
I tried implementing #hEADcRASH's answer: https://stackoverflow.com/a/30924768/4106552, but it doesn't trigger and print anything to the console when the map is moved in the simulator.
Can anyone tell me what I'm doing wrong? I'm new to Swift, so it could be a small error. Also, let me know if there is a lightweight way to solve for restricting the zoom level on a map. I'm worried that the monitor for movement will slow down the map animation a bit. Thanks for the help.
Here is my view controller.
import UIKit
import Parse
import MapKit
class SearchRadiusViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate {
#IBOutlet weak var map: MKMapView!
#IBOutlet weak var menuBtn: UIBarButtonItem!
var locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
//menu button control
if self.revealViewController() != nil {
menuBtn.target = self.revealViewController()
menuBtn.action = "revealToggle:"
self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
}
//user location
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
//set map
let location:CLLocationCoordinate2D = manager.location!.coordinate
let latitude = location.latitude
let longitude = location.longitude
let latDelta:CLLocationDegrees = 0.1
let longDelta:CLLocationDegrees = 0.1
let span:MKCoordinateSpan = MKCoordinateSpanMake(latDelta, longDelta)
let maplocation:CLLocationCoordinate2D = CLLocationCoordinate2DMake(latitude, longitude)
let region:MKCoordinateRegion = MKCoordinateRegionMake(maplocation, span)
map.setRegion(region, animated: true)
//stop updating location, only need user location once to position map.
manager.stopUpdatingLocation()
}
//Attempt to monitor for map movement based on hEADcRASH's answer.
private var mapChangedFromUserInteraction = false
private func mapViewRegionDidChangeFromUserInteraction() -> Bool {
let view = self.map.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) {
print("yes")
mapChangedFromUserInteraction = mapViewRegionDidChangeFromUserInteraction()
if (mapChangedFromUserInteraction) {
// user changed map region
print("user changed map in WILL")
}
}
func mapView(mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
print("yes ddd")
if (mapChangedFromUserInteraction) {
// user changed map region
print("user changed map in Did")
}
}
}
After reviewing and combining a number of other questions/answers and the help of #lorenzoliveto I've got it working in Swift. Please leave a comment if there a better/more lightweight way to achieve the same thing.
I added self.map.delegate = self to the viewDidLoad function.
Below is the code for how I'm monitoring for map movement and then once a user has zoomed in "too far" and the width of the map goes below 2 miles I then zoom out the map using mapView.setRegion.
private var mapChangedFromUserInteraction = false
private func mapViewRegionDidChangeFromUserInteraction() -> Bool {
let view = self.map.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 will change map region
print("user WILL change map.")
// calculate the width of the map in miles.
let mRect: MKMapRect = mapView.visibleMapRect
let eastMapPoint = MKMapPointMake(MKMapRectGetMinX(mRect), MKMapRectGetMidY(mRect))
let westMapPoint = MKMapPointMake(MKMapRectGetMaxX(mRect), MKMapRectGetMidY(mRect))
let currentDistWideInMeters = MKMetersBetweenMapPoints(eastMapPoint, westMapPoint)
let milesWide = currentDistWideInMeters / 1609.34 // number of meters in a mile
print(milesWide)
print("^miles wide")
// check if user zoomed in too far and zoom them out.
if milesWide < 2.0 {
var region:MKCoordinateRegion = mapView.region
var span:MKCoordinateSpan = mapView.region.span
span.latitudeDelta = 0.04
span.longitudeDelta = 0.04
region.span = span;
mapView.setRegion(region, animated: true)
print("map zoomed back out")
}
}
}
func mapView(mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
if (mapChangedFromUserInteraction) {
// user changed map region
print("user CHANGED map.")
print(mapView.region.span.latitudeDelta)
print(mapView.region.span.longitudeDelta)
// calculate the width of the map in miles.
let mRect: MKMapRect = mapView.visibleMapRect
let eastMapPoint = MKMapPointMake(MKMapRectGetMinX(mRect), MKMapRectGetMidY(mRect))
let westMapPoint = MKMapPointMake(MKMapRectGetMaxX(mRect), MKMapRectGetMidY(mRect))
let currentDistWideInMeters = MKMetersBetweenMapPoints(eastMapPoint, westMapPoint)
let milesWide = currentDistWideInMeters / 1609.34 // number of meters in a mile
print(milesWide)
print("^miles wide")
// check if user zoomed in too far and zoom them out.
if milesWide < 2.0 {
var region:MKCoordinateRegion = mapView.region
var span:MKCoordinateSpan = mapView.region.span
span.latitudeDelta = 0.04
span.longitudeDelta = 0.04
region.span = span;
mapView.setRegion(region, animated: true)
print("map zoomed back out")
}
}
UPDATE: 3/7, I discovered an interesting bug in the implementation above. On the simulator it works fine when clicking to zoom, but when you use the pinch to zoom (option + click) the simulator stops allowing you to drag the map around after it animates the zoom back out. This also happened on the beta version on my iphone. I added dispatch_async around the blocks that animate that map back to their position and it appears to be working on the simulator. It no longer appears frozen after it animates and I can continue to drag around the map and try to zoom in.
dispatch_async(dispatch_get_main_queue(), {
var region:MKCoordinateRegion = mapView.region
var span:MKCoordinateSpan = mapView.region.span
span.latitudeDelta = 0.04
span.longitudeDelta = 0.04
region.span = span;
mapView.setRegion(region, animated: true)
print("map zoomed back out")
})
The solution that works for me is one where I set the zoom range. This approach may not have been available at the time the question was asked.
The code fragment below is what I use. I'm not entirely sure what the distance units are, but I believe they are meters. Figuring out what range works in a given instance may be a matter of trial and error.
let mapView = MKMapView(frame: .zero)
let zoomRange = MKMapView.CameraZoomRange(
minCenterCoordinateDistance: 120000,
maxCenterCoordinateDistance: 1600000
)
mapView.cameraZoomRange = zoomRange

Resources