SwiftUI MapKit: How to add a pin to a map (MapAnnotation)

The MapAnnotation class is utilized as an annotation object that ties itself to a specified point on the map. We can use it to create pins with notes on our map.
Written by

Chris C

Updated on

May 01 2024

Table of contents

    The MapAnnotation class is utilized as an annotation object that ties itself to a specified point on the map. We can use it to create pins with notes on our map.

    Pre-requisites

    Creating a MapAnnotation for your map requires two parts. First is the array of locations that you want to add to the map. Second is the design or content of the annotation.

    Start by creating a structure that will be used as the basic skeleton for your map locations. The sample structure conforms to Identifiable because SwiftUI needs a stable identity for every item in the annotation collection.

    import MapKit
    
    struct MapLocation: Identifiable {
        let id = UUID()
        let name: String
        let latitude: Double
        let longitude: Double
    }

    The very basic requirement that a map annotation needs is a coordinate, or more specifically a CLLocationCoordinate2D. You can create that coordinate later from separate latitude and longitude values.

    CLLocationCoordinate2D(latitude: latitude, longitude: longitude)

    Another option is to store the coordinate directly in the model.

    import MapKit
    
    struct MapLocation: Identifiable {
        let id = UUID()
        let name: String
        let coordinate: CLLocationCoordinate2D
    }

    A more user-friendly option is to keep the latitude and longitude values readable, then expose a computed coordinate property that generates the CLLocationCoordinate2D whenever you need it.

    import MapKit
    
    struct MapLocation: Identifiable {
        let id = UUID()
        let name: String
        let latitude: Double
        let longitude: Double
    
        var coordinate: CLLocationCoordinate2D {
            CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
        }
    }

    Doing it this way means you can create a MapLocation using a basic name, latitude, and longitude, then access the coordinate immediately later on.

    Create the location data

    Go back to ContentView.swift and create the data array manually for now. Later, you can modify this if you want the locations to come from an API or file.

    let mapLocations = [
        MapLocation(
            name: "St Francis Memorial Hospital",
            latitude: 37.789467,
            longitude: -122.416772
        ),
        MapLocation(
            name: "The Ritz-Carlton, San Francisco",
            latitude: 37.791965,
            longitude: -122.406903
        ),
        MapLocation(
            name: "Honey Honey Cafe & Crepery",
            latitude: 37.787891,
            longitude: -122.411223
        )
    ]

    Add annotations to the map

    Given the location array, you are ready to feed the items to your Map. If you are not using the user's location, the basic shape looks like this:

    Map(
        coordinateRegion: $region,
        annotationItems: mapLocations,
        annotationContent: { location in
            MapAnnotationProtocol
        }
    )

    In the article sample, the map uses an overload that also tracks and follows the user location.

    Map(
        coordinateRegion: $manager.region,
        interactionModes: MapInteractionModes.all,
        showsUserLocation: true,
        userTrackingMode: $tracking,
        annotationItems: mapLocations,
        annotationContent: { location in
            MapAnnotationProtocol
        }
    )

    MapAnnotationProtocol is used to provide the design and housing of your annotation. Apple provides two basic annotation designs: MapPin and MapMarker. Both require the coordinate for the point you want to show.

    Use MapPin or MapMarker

    Creating a MapPin or MapMarker is straightforward. The main difference is how the annotation looks on the map.

    Map(
        coordinateRegion: $manager.region,
        interactionModes: MapInteractionModes.all,
        showsUserLocation: true,
        userTrackingMode: $tracking,
        annotationItems: mapLocations,
        annotationContent: { location in
            MapPin(coordinate: location.coordinate, tint: .red)
        }
    )
    Map(
        coordinateRegion: $manager.region,
        interactionModes: MapInteractionModes.all,
        showsUserLocation: true,
        userTrackingMode: $tracking,
        annotationItems: mapLocations,
        annotationContent: { location in
            MapMarker(coordinate: location.coordinate, tint: .red)
        }
    )

    These are easy to use, but they do not display a label. That means you cannot identify the points of interest from afar, and you do not get a chance to use the name attribute.

    Create a custom MapAnnotation

    This is where MapAnnotation shines because it gives you the option to design the annotation to your preference.

    MapAnnotation(
        coordinate: location.coordinate,
        content: {
            // design here
        }
    )

    For this example, the annotation displays the location name using Text and shows a marker using an SF Symbol image.

    MapAnnotation(
        coordinate: location.coordinate,
        content: {
            Image(systemName: "pin.circle.fill")
                .foregroundColor(.red)
            Text(location.name)
        }
    )

    You can place that custom annotation inside the map's annotationContent closure.

    Map(
        coordinateRegion: $manager.region,
        interactionModes: MapInteractionModes.all,
        showsUserLocation: true,
        userTrackingMode: $tracking,
        annotationItems: mapLocations,
        annotationContent: { location in
            MapAnnotation(coordinate: location.coordinate) {
                VStack(spacing: 4) {
                    Image(systemName: "pin.circle.fill")
                        .foregroundColor(.red)
                    Text(location.name)
                        .font(.caption)
                        .fixedSize()
                }
            }
        }
    )

    Simulate Location

    Simulating location in SwiftUI is a bit different compared to UIKit. In UIKit, you needed to set the simulated location at the scheme level. In SwiftUI, you can change simulated location on the fly even while the simulator is running.

    Select the Swift file that shows the map in your project, such as ContentView.swift. Then navigate to Debug -> Simulate Location and select a location.

    Tip: when testing annotations that depend on nearby places, simulation makes it easier to verify your map region, visible pins, and user tracking behavior without leaving Xcode.