Learn to Build Your First iOS App

Subscribe below to receive a free 200-page guide that will teach you how to build your first iOS app.

Get a sample book packed with everything you need to start your development journey with Swift and SwiftUI.

    We respect your privacy. Unsubscribe at any time.
    Swift · · 10 min read

    Working with MapKit: Annotations and Shape Rendering

    Working with MapKit: Annotations and Shape Rendering

    Maps are becoming increasingly popular in apps as time goes on. You can see maps in all kinds of apps, from Uber to Instagram to my new app, peek. Although maps are common, adding more complex features to them can be a challenging process. In this tutorial, we’re going to talk about annotations and shape rendering in MapKit, Apple’s map library for iOS. Many of the maps you see on iOS are powered by MapKit, including the map in Apple’s own Maps app! MapKit is a powerful, tried and tested mapping library. It’s also really easy to use, so let’s get started!

    Getting Started

    To get started, download the sample project. Open it, and you’ll find a few things:

    • Places.plist: This is a property list file that stores the places we will be rendering.
    • Places.swift: This Swift file loads the places in Places.plist and exposes them to the rest of our Swift code.
    • Main.storyboard: This is the main storyboard our app uses. If you look into the storyboard, it has a view controller with a map view. I have already set up the UI and auto layout for you.
    • ViewController.swift: This is the view controller that our app will use to control the map. I have already created an outlet that connects with the map view in the storyboard.

    showing-user-location

    Before we can build and run our app, we have to do one more thing: embed MapKit.framework in our project. This is a mandate for using MapKit. You have two ways to do this. One way is to manually add the library. Click on MapKit starter in the project navigator. Once you’ve selected MapKit starter, scroll down to the section that says Linked Frameworks and Libraries:

    embedded-binaries

    Click the plus icon and add MapKit.framework. Do the same in Embedded Binaries if Xcode doesn’t add it for you automatically.

    Alternatively, you can go to the Capabilities tab and switch the Maps option to ON.

    mapkit-capabilities

    Now we’re ready to go! Build and run the app and see what happens! You should see an MKMapView taking up the whole screen. By default, this map view supports zooming, rotating, scrolling, and 3D mode. It also shows points of interest where applicable and displays buildings in 3D in major cities. Cool stuff! We’re going to be working with this map in our tutorial, so grab some coffee, sit down, and let’s get to work!

    Displaying the User’s Location

    First things first, it’s really important to show the user where they are on a map so that they can get a sense of their location relative to the things around them. Luckily, this is pretty easy with MapKit. Open the Storyboard, select our map, and check the box that says User Location:

    showing-user-location

    In case you want to do it programmatically, just place this line of code in the viewDidLoad method of ViewController:

    mapView?.showsUserLocation = true
    

    We need to take a few more steps before this works. Currently, MapKit will try to display the user’s location, but we haven’t requested location access from the user yet! Let’s fix that.

    Open ViewController.swift and add a function called requestLocationAccess():

    func requestLocationAccess() {
        let status = CLLocationManager.authorizationStatus()
        
        switch status {
        case .authorizedAlways, .authorizedWhenInUse:
            return
            
        case .denied, .restricted:
            print("location access denied")
            
        default:
            locationManager.requestWhenInUseAuthorization()
        }
    }
    

    This function has a simple task: it checks the current status of location authorization and then acts accordingly. If the user has already authorized the use of his or her location, it returns, as nothing needs to be done. If location access is denied or restricted, this function prints an error message. Otherwise, we request access to the user’s location when the app is in use.

    One last thing! Head over to Info.plist. We will add a key called NSLocationWhenInUseUsageDescription of type String. Set the following value for this key: “MapKit starter needs your location so that you can see it on a map.” This will tell the user why we are requesting their location, which makes it more likely that they will accept. As of version 8, iOS also requires that we provide this string.

    privacy-plist

    One more step! Add a viewDidLoad() override in ViewController.swift and make it call requestLocationAccess():

    override func viewDidLoad() {
        requestLocationAccess()
    }
    

    Build and run the app again. You should get a prompt asking for access to your location. Press accept, and voila. We can now display the user’s location. MapKit will automatically update this location as the user moves around.

    mapkit-user-location

    If you use the simulator to test the app, you can click the location button in the debug area. Xcode provides a list of locations for you to simulate the user’s location.

    simulate-user-location

    Now, let’s look into displaying some more exciting content.

    Displaying Custom Annotations

    We learned how to display the user’s location already, but now, let’s display a custom annotation with our own image. Open Places.plist and take a look at what’s inside. You should see a brief list of cities. Since this is only for the sake of an example, we kept the list rather short.

    
    
    
        
            title
            New York
            description
            Welcome to New York City!
            latitude
            40.7128
            longitude
            -74.0059
        
        
            title
            San Francisco
            description
            Welcome to San Francisco!
            latitude
            37.7749
            longitude
            -122.4194
        
        
            title
            Los Angeles
            description
            Welcome to Los Angeles!
            latitude
            34.0522
            longitude
            -118.2437
        
    
    
    

    The list has New York, San Francisco, and Los Angeles, along with their latitudes and longitudes. Although property lists are out of the scope of this tutorial, you should check out this tutorial if you want to read more about .plist files.

    If you open Places.swift, you’ll see that it has a static method which retrieves all of the places in Places.plist. You don’t need to worry about the implementation, you just need to know that calling Place.getPlaces() will return all of the places in Places.plist. Let’s start by adding a property to ViewController to store all of the places we load:

    let places = Place.getPlaces()
    

    Just to make sure, here’s what your ViewController.swift file should look like right now:

    import UIKit
    import MapKit
    
    class ViewController: UIViewController {
        @IBOutlet var mapView: MKMapView?
        let locationManager = CLLocationManager()
        
        let places = Place.getPlaces()
    
        override func viewDidLoad() {
            requestLocationAccess()
        }
        
        func requestLocationAccess() {
            let status = CLLocationManager.authorizationStatus()
            
            switch status {
            case .authorizedAlways, .authorizedWhenInUse:
                return
                
            case .denied, .restricted:
                print("location access denied")
                
            default:
                locationManager.requestWhenInUseAuthorization()
            }
        }
    }
    

    Now, let’s get to the good stuff. Here’s the general process for adding annotations to an MKMapView:

    1. Create an object that conforms to the MKAnnotation protocol, or extend an existing one.
    2. Add the annotation object to an instance of MKMapView with addAnnotation(_:)
    3. Create an object that conforms to MKMapViewDelegate and assign that delegate to our map view.
    4. Add an implementation for mapView(_: viewFor:) to provide MapKit with the view to render for any given annotation.

    We’re going to implement this in the 4 unique steps described above.

    Creating an Object to Conform to MKAnnotation

    MKAnnotation is a protocol defined by MapKit that allows us to provide data to MapKit. We’re going to implement the following requirements of MKAnnotation:

    var title: String?
    var subtitle: String?
    var location: CLLocationCoordinate2D
    

    To use the MKAnnotation protocol, you adopt it in any custom objects that store or represent annotation data. Each object then serves as the source of information about a single map annotation and provides critical information, such as the annotation’s location on the map.

    As you might have guessed, we’re going to extend our Place class to conform to MKAnnotation. Import MapKit in Place.swift and add an extension to conform to MKAnnotation:

    extension Place: MKAnnotation { }
    

    Build and run and…nothing? That’s right. Nothing! Conveniently, our Place class already has all the requirements of MKAnnotation.

    Cool. Let’s move onto to the next step.

    Adding the Annotation to a Map View

    This one isn’t tough. We just need to tell mapView to display each of the places we loaded. Let’s add a function to ViewController.swift. We’ll call it addAnnotations():

    func addAnnotations() {
        mapView?.delegate = self
        mapView?.addAnnotations(places)
    }
    

    This isn’t too complex. We set our map view’s delegate to self. Next, we add our place annotations to our map view. Easy! Just make sure to add a call to addAnnotations() in your viewDidLoad() before proceeding.

    Adopting the MKMapViewDelegate Protocol

    The next thing we need is to create an object that conforms to the MKMapViewDelegate protocol. As we did with MKAnnotation, we’re going to extend an existing class to do this for us. If you’ve worked with UITableView before, this should be similar to using UITableViewDelegate and UITableViewDataSource. We use an extension of ViewController to adopt the MKMapViewDelegate protocol like this:

    extension ViewController: MKMapViewDelegate {
        func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
            
        }
    }
    

    This extends our ViewController class and it also adds the function we will use to return a view for each annotation. Let’s go ahead and populate it:

    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        if annotation is MKUserLocation {
            return nil
        }
        
        else {
            let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "annotationView") ?? MKAnnotationView()
            annotationView.image = UIImage(named: "place icon")
            return annotationView
        }
    }
    

    Easy, right? First, we check whether or not the annotation being displayed is the user’s location. If it is, we don’t return our custom annotation, as we want the map to display the user’s location as usual. If it’s not, however, we create a new annotation by dequeuing it. This allows us to reuse annotation views efficiently. Then, we set the annotation view’s image (the image we’re using is in Assets.xcassets). Finally, we return the annotation view.

    Reviewing the Code

    Let’s take one last look at our ViewController.swift. Here’s how it should look if you followed all the steps:

    import UIKit
    import MapKit
    
    class ViewController: UIViewController {
        @IBOutlet var mapView: MKMapView?
        
        let locationManager = CLLocationManager()
        
        let places = Place.getPlaces()
        
        override func viewDidLoad() {
            requestLocationAccess()
            addAnnotations()
        }
        
        func requestLocationAccess() {
            let status = CLLocationManager.authorizationStatus()
            
            switch status {
            case .authorizedAlways, .authorizedWhenInUse:
                return
                
            case .denied, .restricted:
                print("location access denied")
                
            default:
                locationManager.requestWhenInUseAuthorization()
            }
        }
        
        func addAnnotations() {
            mapView?.delegate = self
            mapView?.addAnnotations(places)
        }
    }
    
    extension ViewController: MKMapViewDelegate {
        func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
            if annotation is MKUserLocation {
                return nil
            }
                
            else {
                let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "annotationView") ?? MKAnnotationView()
                annotationView.image = UIImage(named: "place icon")
                return annotationView
            }
        }
    }
    

    If you run the app, you should find three annotations on the map that correspond to the locations specified the Place.plist file.

    mapkit-annotations

    If you’re lost, please go back and check if you missed something. Feel free to comment below if you need help. So far, we learned how to properly add annotations to an MKMapView. We do this with peek to show users where events are located. Most apps that use maps use this technique so that they can show users specific locations.

    Now let’s see how to add some shape rendering.

    Displaying Shapes on Maps

    So, you’ve learned how to display custom annotations that allow you to highlight locations on the map. Next, let’s explore how to draw shapes on the map. For our example, we will be rendering circles with radius of 100m around each of our places. To do this, we need to do two things:

    1. Add overlays to our map.
    2. Specify renderers for each overlay so that MapKit knows how to render the overlays.

    Adding overlays

    First things first, we need to tell MapKit that we want to display overlays on our map. Let’s modify our addAnnotations() functon like this:

    func addAnnotations() {
        mapView?.delegate = self
        mapView?.addAnnotations(places)
    
        let overlays = places.map { MKCircle(center: $0.coordinate, radius: 100) }
        mapView?.addOverlays(overlays)
    }
    

    The first two lines should look familiar, but the last two lines of code are totally new. We start by using map, one of Swift’s collection manipulation functions, to generate an array of MKCircle objects based on our places. If you’re new to map, this is equivalent to writing a for loop. But you can do it in a single line of code.

    Next, we add the overlays using mapView?.addOverlays(_:). This is very similar to adding annotations.

    Rendering Overlays

    Adding overlays is not good enough. MapKit will not display the overlays until you actually render them and make them visible on screen. We added circle overlays for each place, and we should render circles to correspond. Add the mapView(_: rendererFor:) function to ViewController extension:

    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        let renderer = MKCircleRenderer(overlay: overlay)
        renderer.fillColor = UIColor.black.withAlphaComponent(0.5)
        renderer.strokeColor = UIColor.blue
        renderer.lineWidth = 2
        return renderer
    }
    

    As the name specifies, this function allows us to return an MKOverlayRenderer to use for each of our overlays with an appropriate renderer. MapKit has a class for rendering circles, it’s called MKCircleRenderer. We create one of these renderers and configure it to match the appearance we want and then we return the renderer so that MapKit knows to use it.

    Now you’re ready to test your app again. Zoom into New York, San Francisco, or Los Angeles. You will see the circular overlay.

    mapkit-shape-render

    Wrapping Up

    We learned a lot about annotations and shape rendering in this tutorial. Armed with the knowledge you gained, you should be able to make apps that have beautiful maps in them! Feel free to comment if you have any questions.

    For reference, you can download the Xcode project on GitHub.

    Read next