Tutorials
intermediate
Swift

Create a map view compatible with SwiftUI

Prerequisite

Familiarity with Xcode 11 and Swift, and completion of the Mapbox Maps SDK for iOS installation guide.

This guide walks through how to use the Mapbox Maps SDK for iOS with SwiftUI to create a map view, add an annotation to the map, and add a fill layer to the map. SwiftUI is a new Apple framework that provides views, controls, and layout structures for declaring an app’s user interface to simplify an app’s creation.

In this guide you’ll learn how to use several SwiftUI concepts with Mapbox, including:

  • Integrating an instance of MGLMapView to display a map view.
  • Using state variables and bindings to coordinate view updates.
  • Using a Coordinator to implement MGLMapViewDelegate to integrate the Mapbox Maps SDK for iOS with SwiftUI.

Getting started

This guide assumes you are familiar with Swift and Xcode. Here are the resources you’ll need before getting started:

  • A SwiftUI enabled application. In Xcode, create a SwiftUI project. Select File > New > Project and select User Interface: SwiftUI.
Screenshot of Xcode 11 with the options to create a SwiftUI project

File structure

In this tutorial, you will work with two files:

  • ContentView.swift: This is the SwiftUI view that is generated when you create a new SwiftUI project. It contains a structure that conforms to the View protocol and describes the view’s content and layout.
  • MapView.swift: This is a custom SwiftUI view where you will add and configure the map. You'll use this map view inside ContentView.swift.

Display a map

First, you'll display a map with a specified style, center point, and zoom level.

Create a MapView SwiftUI view

To represent an MGLMapView in SwiftUI, create a wrapping struct that conforms to the UIViewRepresentable protocol. This struct will create, and configure MGLMapView, while SwiftUI manages its life cycle and updates.

Create a new SwiftUI view file, named MapView.swift. This is where you'll configure your Mapbox map.

Start by importing Mapbox. Then, declare the MapView type as conforming to UIViewRepresentable, a view that represents a UIKit view. UIViewRepresentable requires two protocol methods: makeUIView and updateUIView. To present a map as the view, add a private MGLMapView property using the Mapbox Streets map style, and return it in makeUIView. For now, define updateUIView, but do not return anything. This will change when adding annotations and layers to the map.

MapView.swift
import SwiftUI
import Mapbox

struct MapView: UIViewRepresentable {
    private let mapView: MGLMapView = MGLMapView(frame: .zero, styleURL: MGLStyle.streetsStyleURL)

    func makeUIView(context: UIViewRepresentableContext<MapView>) -> MGLMapView {
        return mapView
    }
    
    func updateUIView(_ uiView: MGLMapView, context: UIViewRepresentableContext<MapView>) {

    }
}

Configure the map

In this step you'll prepare MapView to receive the values for several modifiers from ContentView where the actual values will be specified. These modifiers will determine the appearance of the map:

  • styleURL for setting the map style
  • centerCoordinatepecifying where the map is centered
  • zoomLevel for specifying the zoom level of the initial view
MapView.swift
struct MapView: UIViewRepresentable {
    private let mapView: MGLMapView = MGLMapView(frame: .zero, styleURL: MGLStyle.streetsStyleURL)
    
    func makeUIView(context: UIViewRepresentableContext<MapView>) -> MGLMapView {
        mapView.delegate = context.coordinator
        return mapView
    }
    
    func updateUIView(_ uiView: MGLMapView, context: UIViewRepresentableContext<MapView>) {

    }
    
    func styleURL(_ styleURL: URL) -> MapView {
        mapView.styleURL = styleURL
        return self
    }
    
    func centerCoordinate(_ centerCoordinate: CLLocationCoordinate2D) -> MapView {
        mapView.centerCoordinate = centerCoordinate
        return self
    }
    
    func zoomLevel(_ zoomLevel: Double) -> MapView {
        mapView.zoomLevel = zoomLevel
        return self
    }
}

Configure the ContentView

In ContentView, specify MapView. Then, add and provide values for the centerCoordinate and zoomLevel modifiers.

ContentView.swift
import SwiftUI
import Mapbox

struct ContentView: View {
    
    var body: some View {
        MapView().centerCoordinate(.init(latitude: 37.791293, longitude: -122.396324)).zoomLevel(16)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Build the project, and you will see a map centered on San Francisco.

Add an annotation to the map

Next, you'll use @State to add a single marker to the map that displays a callout when tapped.

To do this, you’ll declare a @State property in ContentView.swift, and pass a binding to this property down to the MapView. The MapView updates the binding to match the visible page.

Create an MGLPointAnnotation extension

Create an MGLPointAnnotation extension with a convenience initializer that will let you create an annotation using a title and a coordinate.

MapView.swift
import SwiftUI
import Mapbox

extension MGLPointAnnotation {
    convenience init(title: String, coordinate: CLLocationCoordinate2D) {
        self.init()
        self.title = title
        self.coordinate = coordinate
    }
}

struct MapView: UIViewRepresentable {
    ...
}

Add a Coordinator class

In SwiftUI, a Coordinator can be used with delegates, data sources, and user events. In this example, you'll use a delegate, MGLMapViewDelegate, to add the annotation view to the map. In order for a SwiftUI view to implement MGLMapViewDelegate it must declare a Coordinator class.

Within MapView, define a nested Coordinator class.

MapView.swift
final class Coordinator: NSObject, MGLMapViewDelegate {
    var control: MapView
    
    init(_ control: MapView) {
        self.control = control
    }
    
    func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
        return nil
    }
        
    func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
        return true
    }
}

Integrate a property binding to add annotations.

Then, add another method called makeCoordinator to MapView to make the coordinator.

MapView.swift
@Binding var annotations: [MGLPointAnnotation]

func updateUIView(_ uiView: MGLMapView, context: UIViewRepresentableContext<MapView>) {
    updateAnnotations()
}

func makeCoordinator() -> MapView.Coordinator {
    Coordinator(self)
}

private func updateAnnotations() {
    if let currentAnnotations = mapView.annotations {
        mapView.removeAnnotations(currentAnnotations)
    }
    mapView.addAnnotations(annotations)
}

Configure the ContentView

In the generated file ContentView.swift create an annotation array variable that will bind changes to the MapView’s annotations. Create a MapView and bind the annotation’s data.

ContentView.swift
import SwiftUI
import Mapbox

struct ContentView: View {
    
    @State var annotations: [MGLPointAnnotation] = [
        MGLPointAnnotation(title: "Mapbox", coordinate: .init(latitude: 37.791434, longitude: -122.396267))
    ]
    
    var body: some View {
        MapView(annotations: $annotations).centerCoordinate(.init(latitude: 37.791293, longitude: -122.396324)).zoomLevel(16)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Build and run your application, and you will see a map centered on San Francisco with a marker at the center of the map.

Add a layer to the map

Inside the Coordinator class, implement the delegate methods to add a fill layer when the style finishes loading.

MapView.swift
final class Coordinator: NSObject, MGLMapViewDelegate {
    var control: MapView
    
    init(_ control: MapView) {
        self.control = control
    }
    
    func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) {
        
        let coordinates = [
            CLLocationCoordinate2D(latitude: 37.791329, longitude: -122.396906),
            CLLocationCoordinate2D(latitude: 37.791591, longitude: -122.396566),
            CLLocationCoordinate2D(latitude: 37.791147, longitude: -122.396009),
            CLLocationCoordinate2D(latitude: 37.790883, longitude: -122.396349),
            CLLocationCoordinate2D(latitude: 37.791329, longitude: -122.396906),
        ]
        
        let buildingFeature = MGLPolygonFeature(coordinates: coordinates, count: 5)
        let shapeSource = MGLShapeSource(identifier: "buildingSource", features: [buildingFeature], options: nil)
        mapView.style?.addSource(shapeSource)
        
        let fillLayer = MGLFillStyleLayer(identifier: "buildingFillLayer", source: shapeSource)
        fillLayer.fillColor = NSExpression(forConstantValue: UIColor.blue)
        fillLayer.fillOpacity = NSExpression(forConstantValue: 0.5)
        
        mapView.style?.addLayer(fillLayer)

    }
    
    func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
        return nil
    }
     
    func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
        return true
    }
    
}

Build the project, and you will see a map centered on San Francisco with a single marker and a filled rectangle on the map.

Finished product

You have an app that displays an MGLMapView with a fill layer and a point annotation. View the SwiftUI project for this tutorial on GitHub.

Was this page helpful?