Create a map view compatible with SwiftUI
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 implementMGLMapViewDelegate
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.
- Include the Mapbox Maps SDK for iOS. If you're new to the Maps SDK for iOS, complete the Installation steps with the Mapbox Maps SDK for iOS guide.
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 theView
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 insideContentView.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.
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 stylecenterCoordinate
pecifying where the map is centeredzoomLevel
for specifying the zoom level of the initial view
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.
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
.
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.
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.
@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.
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.
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.