As I mentioned in another extension post, I dig them…when they make sense. I tend to do simple map stuff a good bit and wanted to write an extension for it.
Swift: Handle Location and MapView Updates with Extension on UIViewController
What it does:
- Requests authorization for user location
- Centers the map on the user
- Switches the map type from button taps
- Drops a pin from a long tap
What it requires:
- MapKit framework
- NSLocationWhenInUseUsageDescription setting in your project settings
- UI in Interface Builder w/ a MKMapView (tag: 999)
- Extension code (below)
- Longtap gesture dropped on the map with the action set to the view controller
- Optional: buttons to change the map type (tags: 0, 1 and 2 – action connected to view controller)
- View controller class with connection to the map view and a CLLocationManager instance (MapKit and CoreLocation added to project)
Full code at the bottom of this post.
Steps:
Some steps are important to do in order. However, after you’ve done this you might see that it could make sense to add the code after adding step 2 above.
MapKit
Go to your project Capabilities and turn on Maps:
This adds the MapKit.framework to your project. You could do it manually, but… oh, well.
NSLocationWhenInUseUsageDescription
While you’re around there, go to the Info tab and add a new entry for NSLocationWhenInUseUsageDescription. Make the value whatever you want (descriptive and not snarky).
MapView in Interface Builder
Of course, your view controller in the UI needs to have its class set to whatever class you are defining in the code. In my example code below my class name is MapVC.
In your project’s UI, add a MapView to your view controller (drag from your Object Library on the bottom right of Xcode). Connect the MapView’s delegate to your view controller.
In the MapView’s attributes, set the tag to 999.
Extension Code
We’ll come back to the UI, but let’s get to the code now.
Show User Location
First we’ll create the extension file (⌘+n) as an iOS>Source>Swift file named whatever you want.
import Foundation import MapKit extension UIViewController : MKMapViewDelegate, CLLocationManagerDelegate, UIGestureRecognizerDelegate { func showUserLocation(locationManager : CLLocationManager, mapView : MKMapView) { locationManager.delegate = self if CLLocationManager.authorizationStatus() != .AuthorizedWhenInUse { locationManager.requestWhenInUseAuthorization() } else { mapView.showsUserLocation = true } }
Import MapKit and declare that it adopts the protocols MKMapViewDelegate, CLLocationManagerDelegate and UIGestureRecognizerDelegate.
The first function takes a location manager and map view. Those should be from your view controller class. The map view is an outlet created when you drag it into the code and the location manager is created in your view controller like:
let locationManager = CLLocationManager()
More on that later when the code is ready, but… meanwhile…
The showUserLocation function sets the delegate on the location manager and then either requests authorization for location (‘when in use’ you could use ‘always’) or starts the map view displaying the user’s location.
Location Authorization
If the user needed to be prompted for authorization, we have this code to handle their response:
public func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) { switch status { case .AuthorizedWhenInUse, .AuthorizedAlways: if let mapView = self.view.viewWithTag(999) as? MKMapView { // set tag in IB centerOnUser(mapView) } default: let aa = UIAlertController.init(title: "Authorization", message: "Please go into Settings and give this app " + "authorization to your location.", preferredStyle: .Alert) aa.addAction(UIAlertAction.init(title: "OK", style: .Default, handler: nil)) self.presentViewController(aa, animated: true, completion: nil) } }
If the user approved, it finds the map view via the tag and calls centerOnUser passing in the map view. Otherwise, it informs the user it needs the location.
Center On User
public func mapView(mapView: MKMapView, didUpdateUserLocation userLocation: MKUserLocation) { mapView.showsUserLocation = true centerOnUser(mapView) } func centerOnUser(mapView : MKMapView) { var region = MKCoordinateRegion() region.center = mapView.userLocation.coordinate var span = MKCoordinateSpan() span.latitudeDelta = 0.05 span.longitudeDelta = 0.05 region.span = span mapView.setRegion(region, animated: true) }
If the user did authorize, the map view will send location updates to the delegate (which we set to be self). The didUpdateUserLocation function calls centerOnUser with the passed in map view.
centerOnUser does just that with a span lat/long of 0.05 which is fairly close. You can change that to what you need.
Map Type Changing
If you want to allow the user to change the map type, you can use this action…
@IBAction func doBtnMap(sender: UIButton) { if let mapView = self.view.viewWithTag(999) as? MKMapView { // set tag in IB mapView.mapType = MKMapType.init(rawValue: UInt(sender.tag))! } }
It changes the map type create three buttons (maybe on a toolbar) and set the tags on the buttons to be: 0 for Standard, 1 for Satellite and 2 for hybrid.
Set the action on the buttons (“Sent Action” for UIBarButtonItem or “Touch Up Inside” for UIButton) to be the doBtnMap: function above. More on that when we get back to the UI below.
There are other ways to do this same type of thing possibly with one button with tag 0 and increment it when it’s tapped (as long as you reset to 0 when the tag is 2).
Long Tap for Annotation
To drop an annotation for a long tap (re: long tap gesture), use this code:
@IBAction func handleLongTap(gestureReconizer: UILongPressGestureRecognizer) { if let mapView = self.view.viewWithTag(999) as? MKMapView { // set tag in IB // remove current annons except user location let annotationsToRemove = mapView.annotations.filter { $0 !== mapView.userLocation } mapView.removeAnnotations( annotationsToRemove ) // add new annon let location = gestureReconizer.locationInView(mapView) let coordinate = mapView.convertPoint(location, toCoordinateFromView: mapView) let annotation = MKPointAnnotation() annotation.coordinate = coordinate mapView.addAnnotation(annotation) } }
It finds the map view, removes existing annotations (except for the user’s location annotation), creates a new annotation from the tap location converted to a coordinate and adds it to the map view.
User Interface
Going back to the UI in Interface Builder…
Long Tap Gesture
Drop a long tap gesture (from the Object Library) on the map view and set it’s action (drag from the empty circle under Sent Actions to the view controller icon (yellow circle w/ white square on the left of the image below)) select the handleLongTap: action defined in the code (above).
Map Type Buttons
If you want the map type buttons you can create a toolbar or similar and set their tags. Set their Sent Action (UIBarButtonItem) or Touch Up Inside event (UIButton) to be the doBtnMap: action defined in the code.
Your View Controller
Your view controller doesn’t need to do much. It has a map view property created when you dragged it… drug it… drugged it… that’s not right… when you did drag it into the code. 🙂
You need to create you location manager. And you need to call showUserLocation: at some point.
Your whole view controller class could be this.
class MapVC : UIViewController { @IBOutlet weak var mapView: MKMapView! let locationManager = CLLocationManager() override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) showUserLocation(locationManager, mapView: mapView) } }
I actually override the two methods: didUpdateUserLocation and handleLongTap to do some extra processing specific to this project. I call the super version in each case though.
So the full code for the extension is:
import Foundation import MapKit extension UIViewController : MKMapViewDelegate, CLLocationManagerDelegate, UIGestureRecognizerDelegate { func showUserLocation(locationManager : CLLocationManager, mapView : MKMapView) { locationManager.delegate = self if CLLocationManager.authorizationStatus() != .AuthorizedWhenInUse { locationManager.requestWhenInUseAuthorization() } else { mapView.showsUserLocation = true } } public func mapView(mapView: MKMapView, didUpdateUserLocation userLocation: MKUserLocation) { mapView.showsUserLocation = true centerOnUser(mapView) } func centerOnUser(mapView : MKMapView) { var region = MKCoordinateRegion() region.center = mapView.userLocation.coordinate var span = MKCoordinateSpan() span.latitudeDelta = 0.05 span.longitudeDelta = 0.05 region.span = span mapView.setRegion(region, animated: true) } public func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) { switch status { case .AuthorizedWhenInUse, .AuthorizedAlways: if let mapView = self.view.viewWithTag(999) as? MKMapView { // set tag in IB centerOnUser(mapView) } default: let aa = UIAlertController.init(title: "Authorization", message: "Please go into Settings and give this app " + "authorization to your location.", preferredStyle: .Alert) aa.addAction(UIAlertAction.init(title: "OK", style: .Default, handler: nil)) self.presentViewController(aa, animated: true, completion: nil) } } @IBAction func doBtnMap(sender: UIButton) { if let mapView = self.view.viewWithTag(999) as? MKMapView { // set tag in IB mapView.mapType = MKMapType.init(rawValue: UInt(sender.tag))! } } @IBAction func handleLongTap(gestureReconizer: UILongPressGestureRecognizer) { if let mapView = self.view.viewWithTag(999) as? MKMapView { // set tag in IB // remove current annons except user location let annotationsToRemove = mapView.annotations.filter { $0 !== mapView.userLocation } mapView.removeAnnotations( annotationsToRemove ) // add new annon let location = gestureReconizer.locationInView(mapView) let coordinate = mapView.convertPoint(location, toCoordinateFromView: mapView) let annotation = MKPointAnnotation() annotation.coordinate = coordinate mapView.addAnnotation(annotation) } } }