If you need to show a map to your users in iOS, one way to accomplish this is to use the MKMapView class. Mix this with the MKMapViewDelegate and you have the ability to detect movement on the map, detect the state of the map such as failure loading it as well as several other items you can monitor.
In this tutorial we’ll add a map to the view, allow the current location of the phone to be zoomed to, and allow the user to switch between the different map types which are available meaning Standard, Satellite, and Hybrid. We will also learn how to implement a delegate method and have the map move each time the user moves.
MKMapView Tutorial
The first step is to create a Single View Application by opening Xcode and creating the new project. You can follow this tutorial if you are unsure how to create a new project. Make sure you select the language as Swift. You can uncheck unit tests, core data, and other optional extras.
Navigate to Main.storyboard and drag out a UIToolBar and seat it at the bottom of the view. Click the UIBarButtonItem (named Item) and rename the text attribute to “Current Location”. Drag out a Segment control and put that on the toolbar also. You might choose to use a flexible spacer between the button on the left and the segment to force them to the left and right edges of the toolbar.
Select the Segment View and change the segments to 3. Rename each segment as follows:
Segment 0: Standard
Segment 1: Satellite
Segment 2: Hybrid
The next task is to drag a Map Kit View in to the view. I made mine fill the rest of the view above the toolbar.
When complete you should have something similar to that shown above.
We now need to create some IBOutlet and IBAction properties. Open up the assistant editor which is found on the top toolbar over on the far right just (second set of controls from the right). Click the middle button which is 2 intersecting circles.
Ctrl+drag from the MapView to just below the class declaration to create an IBOutlet. Call it mapView and hit connect.
Do the same from the current location and segment control. I dragged to just below viewDidLoad. Instead of leaving “outlet” as the default you need to change the connection to “Action”. I also changed the type to UISegmentedControl and gave it the name of mapTypeChanged. Click connect when done.
You can do the same for the Current Location button, but rather than selecting the type as UISegmentedControl (which isn’t available) you can select UIBarButtonItem from the dropdown.
Implementing MapView in the ViewController.swift Class
The storyboard is complete. It’s time to write some code to get the project working.
Step 1:
First we import MapKit as well as CoreLocation. The reason we need to implement CoreLocation is so that we can use it to request authorisation for using the users location.
import MapKit import CoreLocation
Create a property for a CLLocationManager as follows (which can be put just below the mapView IBOutlet):
var locationManager = CLLocationManager.init()
In viewDidLoad add the following:
locationManager.requestWhenInUseAuthorization() mapView.mapType = .standard mapView.showsUserLocation = true mapView.showsScale = true mapView.showsCompass = true
Line 1 is where we request from the user permission to use their location. As well as adding this line, you also need to add a key and description to the info.plist file for your app. The key to add is:
NSLocationWhenInUseUsageDescription
You can set the string description as anything you like such as “This app uses location so it can show where you are currently on a map”. Please use something better written if you publish your app.
On the next lines of code we next set the mapType to .standard which is actually already defaulted to this anyway. We also set .showsUserLocation to true as well as a few other items. I added these just to demonstrate what can be enabled or disabled. You can make these changes in the storyboard as well by clicking on the map view and setting attributes that way. You can opt to set both in code as well as in the attributes section in the storyboard. What you set in the storyboard will get overridden by what you set in code.
When you run the app you will now see a map that shows your current location although you might not be able to see that on screen. You will need to scroll around to find the blue dot if your country is out of view. If you are using a simulator make sure you enable location in the Debug menu for the simulator.
Implementing the Current Location Button and the Segmented Control
You should have 2 IBAction methods available in the class. One will be called something like zoomToCurrentLocation: and the other mapTypeChanged:. These were added when you ctrl+dragged from the storyboard earlier.
Lets start with zoomToCurrentLocation:.
To zoom to the users current location we need to create an MKCoordinateSpan and then create an MKCoordinateRegion and then set the region on the mapView.
@IBAction func zoomToCurrentLocation(_ sender: UIBarButtonItem) { let span = MKCoordinateSpan.init(latitudeDelta: 0.0075, longitudeDelta: 0.0075) let region = MKCoordinateRegion.init(center: (locationManager.location?.coordinate)!, span: span) mapView.setRegion(region, animated: true) }
On line 2 we create an MKCoordinateSpain and initialise it with a latitude delta and a longitude delta of which I selected 0.0075 for each. In practice, these numbers will vary a little (specifically the longitude delta) as you move north and south on the globe, although MapKit will still show a map even when the values are not quite right.
The documentation tells us that 1 degree is approximately 69 miles for latitude meaning that 0.0075 is about half a mile. Longitude delta is different because the higher up the globe you are, the smaller the distance between the degrees, so on the equator 0.0075 will also be about half a mile, but further north the east and west range is smaller per degree. I’ll save the specifics for another tutorial.
You might also opt to make the deltas variables here so that you can pass numbers in such as a zoom level. You might implement a slider and each time you tap current location it can take that slider value and pass that in to the lat and lng deltas to provide a zoom level.
Line 3 is where we set the region. An MKCoordinateRegion requires a span which we set on line 2 and it also requires the location. For this, we are using the current location taken from the locationManager. MapKit also has a current location property, but in this instance I opted to use the one from locationManager. Note that location is optional on the locationManager, so you might want to check for nil values if you are implementing this in production as it may crash if nothing is provided when it expected something.
On line 4 we set the region and set animated to true meaning that when you zoom to the location it will animate there rather than just appear.
Next we need to implement mapTypeChanged. If you ctrl-dragged from the segmented control and not the UIBarButtonItem that it was embedded in, you should have a sender of type UISegmentedControl. We need to implement the following:
@IBAction func mapTypeChanged(_ sender: UISegmentedControl) { switch sender.selectedSegmentIndex { case MapType.StandardMap.rawValue: mapView.mapType = .standard case MapType.SatelliteMap.rawValue: mapView.mapType = .satellite case MapType.HybridMap.rawValue: mapView.mapType = .hybrid default: break } }
I opted to use a switch here and check for the selectedSegmentIndex. At the top of the class I also created an enum with some values so that we can check by a more friendly name rather than just specifying a 0, 1, or a 2. I did this as follows:
enum MapType: NSInteger { case StandardMap = 0 case SatelliteMap = 1 case HybridMap = 2 }
So on the mapTypeChanged method I used the switch to select which segment was selected. I then set the mapType to the appropriate map type selected.
When you test the app now you should be able to zoom to your current location (with a view of just over half a mile), and be able to change the map type by selecting it from the segmented control.
MKMapViewDelegate
It’s time to look at the delegate and see what we can do with that. For this example we will use something simple. If the current location of the user changes, the map will move and centre on that location.
To do this, we need to adopt the MKMapViewDelegate by adding it to the class declaration like so:
class ViewController: UIViewController, MKMapViewDelegate {
None of the delegate methods are required in MKMapViewDelegate which means that no warnings will appear once the delegate is specified. Lets go ahead and implement mapView:didUpdate userLocation as follows:
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) { }
A quick way to add delegate methods is to start typing the function name as found in the documentation. We can see that it begins with mapView, so we type mapView and code completion will bring a list of available methods as follows:
Highlight the one you want and hit enter. If code completion disappears, move the curser to where you typed and hit escape.
We need to set the delegate to self next. You can do this in viewDidLoad by adding the following line:
mapView.delegate = self
If you put the following in the delegate method you just added in the step before this, each time you change location (either in the simulator or on a physical device) it will print test to the view although this isn’t immediate and sometimes takes a few seconds which is fine.
print("test")
After confirming that the delegate method is being called, you can go ahead and delete the print statement.
We now want to centre the map on the current location of the device automatically using this delegate method:
Add the following to the delegate method:
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) { mapView .setCenter(userLocation.coordinate, animated: true) }
This line uses the setCentre method to move the map to a centre coordinate and animate the transition to that coordinate. Note that it doesn’t zoom the map at this point. The map stays at it’s current span.
If you were to implement this particular delegate method in a production app you would likely not implement it as basic as we have here. This tutorial is merely just scratching the surface. Instead, you might consider checking how far the user has moved and if it’s within a few meters, you might just choose to keep the map still and let the dot move around the view a little.
You can download the project here and find the full source code below:
ViewController.swift
import UIKit import MapKit import CoreLocation enum MapType: NSInteger { case StandardMap = 0 case SatelliteMap = 1 case HybridMap = 2 } class ViewController: UIViewController, MKMapViewDelegate { @IBOutlet weak var mapView: MKMapView! var locationManager = CLLocationManager.init() override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. locationManager.requestWhenInUseAuthorization() mapView.delegate = self // Some of these are not too important but just give // a representation of what is possible. mapView.mapType = .standard mapView.showsUserLocation = true mapView.showsScale = true mapView.showsCompass = true } @IBAction func zoomToCurrentLocation(_ sender: UIBarButtonItem) { let span = MKCoordinateSpan.init(latitudeDelta: 0.0075, longitudeDelta: 0.0075) let region = MKCoordinateRegion.init(center: (locationManager.location?.coordinate)!, span: span) mapView.setRegion(region, animated: true) } @IBAction func mapTypeChanged(_ sender: UISegmentedControl) { switch sender.selectedSegmentIndex { case MapType.StandardMap.rawValue: mapView.mapType = .standard case MapType.SatelliteMap.rawValue: mapView.mapType = .satellite case MapType.HybridMap.rawValue: mapView.mapType = .hybrid default: break } } func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) { mapView .setCenter(userLocation.coordinate, animated: true) } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } }
Mary says
Thank you for a good practice!
Robert says
Hi Matthew,
I’m getting an error on unwrapping the locationManager.location optional from your third section on the command “let region = MKCoordinateRegion.init(center: (locationManager.location?.coordinate)!, span: span)”. The error is as follows: “Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)”. I’ve tried recreating the instructions in a completely new and clean project with the same error. Any suggestions?
Matthew says
Can you paste your full code in to here and I’ll take a look. I will also check on my side on the current version of Xcode and see if there’s some differences with a newer version of Swift.
Robert S. Smith, Ph.D. says
Hello –
Thanks for this tutorial! My build is good and I have used your identical code, However, the zoom doesn’t work and my “current location” is San Francisco when I am actually south of LA!
RS
Matthew says
Are you using this on an iOS device, or in the simulator?
darkmoody says
Thanks for the article, super helpful. Here’s a MapKit Tutorial updated to Swift 5, for those building apps on the latest versions of Swift.