NEW: Join my free 100 Days of SwiftUI challenge today! >>

Communicating with a MapKit coordinator

Paul Hudson    @twostraws   

It’s trivial to embed an empty MKMapView into SwiftUI, but if you want to do anything useful with the map then you need to introduce a coordinator – a class that can act as the delegate for your map view, passing data to and from SwiftUI.

Just like working with UIImagePickerController, this means creating a nested class that inherits from NSObject, making it conform to whatever delegate protocol our view or view controller works with, and giving it a reference to the parent struct so it can pass data back up to SwiftUI.

For map views, the protocol we care about is MKMapViewDelegate, so we can start writing a coordinator class immediately. Add this as a nested class inside MapView:

class Coordinator: NSObject, MKMapViewDelegate {
    var parent: MapView

    init(_ parent: MapView) {
        self.parent = parent
    }
}

With that class in place our code will stop compiling, because SwiftUI can see we’ve got a coordinator class and wants to know how it should be created.

Just as with the UIViewControllerRepresentable protocol, that means adding a method called makeCoordinator() that sends back a configured instance of our Coordinator. This should be added to the MapView struct, and it will send itself to the coordinator so it can report back what’s happening.

So, add this method to MapView now:

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

We can now connect that to our MKMapView by adding this line of code to the makeUIView() method:

mapView.delegate = context.coordinator

That completes our configuration, which means we can now start adding methods that make our coordinator respond to activity in the map view. Remember, our coordinator is the delegate of the map view, which means when something interesting happens it gets notified – when the map moves, when it starts and finishes loading, when the user was located on the map, when a map pin was touched, and so on.

MapKit automatically examines our coordinator class to see which one of those notifications we want to be told about. It does this using function signatures: if it finds a method with a precise name and parameter list, it will call that.

To demonstrate this, we’re going to add a method called mapViewDidChangeVisibleRegion() that takes a single MKMapView parameter. Yes, this method name is very long, but trust me there are many longer out there in UIKit – my personal favorite got deprecated way back in iOS 5.0, and was called willAnimateSecondHalfOfRotationFromInterfaceOrientation()!

Anyway, add this method to the Coordinator class now:

func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
    print(mapView.centerCoordinate)
}

That will be called whenever the map view changes its visible region, which means when it moves, zooms, or rotates. All we’ve made it do is print the new center coordinate, so when you run the app back in the simulator you should see lots of coordinates being printed in the Xcode output window.

Map view coordinators are also responsible for providing more information when the map view needs it. For example, we can add annotations to a map, which act as points of interest that we want users to interact with. This is model data, meaning that it’s just a title and some coordinates as opposed to a visual representation of that data, and so when the map view wants to render our annotations it will ask the coordinator what should be shown.

To demonstrate this, we’re going to modify the makeUIView() method so that we send in an annotation for the city of London, like this:

func makeUIView(context: Context) -> MKMapView {
    let mapView = MKMapView()
    mapView.delegate = context.coordinator

    let annotation = MKPointAnnotation()
    annotation.title = "London"
    annotation.subtitle = "Capital of England"
    annotation.coordinate = CLLocationCoordinate2D(latitude: 51.5, longitude: 0.13)
    mapView.addAnnotation(annotation)

    return mapView
}

MKPointAnnotation is a class that conforms to the MKAnnotation protocol, which is what MapKit uses to display annotations. You can create your own annotation types if you want, but MKPointAnnotation is good enough here because it lets us provide a title, subtitle, and coordinate. If you were curious, the name CLLocationCoordinate2D starts with “CL” because it comes from another Apple framework called Core Location.

Anyway, that adds an annotation to our map, and with no further work you should be able to run the app again, then scroll around until you find London – you should see a marker there that can be tapped to reveal our subtitle.

If you want to customize the way that marker looks, we need to bring our coordinator back into play. The map view will look in our coordinator for a particular method called mapView(_:viewFor:), and it will be called if it exists. This can create a custom annotation view, but again Apple gives us a neat alternative in the form of MKPinAnnotationView.

Add this code to the Coordinator class:

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    let view = MKPinAnnotationView(annotation: annotation, reuseIdentifier: nil)
    view.canShowCallout = true
    return view
}

As you can see, that method gets handed a map view and an annotation, and must return the correct view to use to display that annotation. In our code, we use that to create an instance of MKPinAnnotationView, passing it the annotation it should work with, we then set canShowCallout to true so that tapping the pin shows information, then send it back.

Before we finish up with maps for now, I want to briefly mention that reuseIdentifier property. Creating views is expensive, which is why SwiftUI has things like the Identifiable protocol – if it can identify its views uniquely then it can tell which ones have changed and which haven’t, which means it minimizes the amount of work it needs to do.

Frameworks such as UIKit and MapKit has a simpler version of the same concept, called reuse identifiers. These are strings that can be anything we want, and allow the framework to keep a big array of views ready to be reused. We can ask for one with a specific ID – “give me a pin with the identifier Placemark” – and get one back from the array ready to be used, which means we don’t need to create it again.

We specified nil as the reuse identifier above, which means we don’t want to reuse views. This is fine when you’re just learning – and realistically at any time when you’re only going to use a handful of pins – but later on I’ll be showing you the more efficient route here, which means reusing views.

LEARN SWIFTUI FOR FREE I have a massive, free SwiftUI video collection on YouTube teaching you how to build complete apps with SwiftUI – check it out!

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.0/5