Skip to content

Swift: Handle Location and MapView Updates with Extension on UIViewController

map view uiviewcontroller extension

map view uiviewcontroller extension
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:

  1. Requests authorization for user location
  2. Centers the map on the user
  3. Switches the map type from button taps
  4. Drops a pin from a long tap

What it requires:

  1. MapKit framework
  2. NSLocationWhenInUseUsageDescription setting in your project settings
  3. UI in Interface Builder w/ a MKMapView (tag: 999)
  4. Extension code (below)
  5. Longtap gesture dropped on the map with the action set to the view controller
  6. Optional: buttons to change the map type (tags: 0, 1 and 2 – action connected to view controller)
  7. 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:

Xcode capabilities - map

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).

NSLocationWhenInUseUsageDescription in project settings

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.

map view tag 999

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).

long tap gesture

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.

map type buttons with tags

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

}