Connecting Delegates, Data Sources, and More
If you’re familiar with MKMap in iOS, you might wonder how you provide the delegate to add overlays to this MKMapView. If you try accessing data in a SwiftUI struct directly from UIKit, your app crashes. Instead, you have to create a Coordinator class that inherits from NSObject.
This class acts as a transition or bridge between the data in SwiftUI and the external framework. Recall the code for the makeUIView(context:) method:
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
You can see context passed as the first parameter. You’ll also see context passed as the second parameter in the updateUIView(_:context:) method. Add the following code for the new class at the top of FlightMapView.swift, before the FlightMapView struct:
class MapCoordinator: NSObject {
var mapView: FlightMapView
var fraction: CGFloat
init(
_ mapView: FlightMapView,
progress: CGFloat = 0.0
) {
self.mapView = mapView
self.fraction = progress
}
}
This small class takes care of two needs when integrating your SwiftUI app with MapKit features that are available in UIKit. The custom initializer gives you a way to pass flight information to the class. This Coordinator lets you connect the MapKit delegate methods and gives a way for data to flow between SwiftUI and UIKit and a place to implement supporting methods that your UIKit object needs.
You must tell SwiftUI about the Coordinator class. Add the following code to the FlightMapView struct after makeUIView(context:):
func makeCoordinator() -> MapCoordinator {
MapCoordinator(self, progress: progress)
}
This method creates the coordinator and returns it to the SwiftUI framework to pass in where necessary. SwiftUI calls makeCoordinator() before makeUIView(context:), so the controller is available during the creation and configuration of your non-SwiftUI components.
MapKit provides the MKGeodesicPolyline to represent this shape, but this overlay can’t be used in a MapView. An MKGeodesicPolyline creates a shape that follows Earth’s contours along the shortest path between points. As mentioned earlier, the movement from the Earth’s curved surface to the flat map distorts shapes. An MKGeodesicPolyline reflects the shortest path over the Earth. It often appears curved when shown on a flat map. It also provides a good representation of the route a plane would take flying between two points.
To add this overlay, you must add a delegate. In updateUIView(_:context:), add the following code just before the section marked with // 2 at the top of the method:
// 1
let distance = startPoint.distance(to: endPoint)
let cityRadius = distance / 100.0
// 2
let startOverlay = MKCircle(
center: startCoordinate,
radius: cityRadius
)
let endOverlay = MKCircle(
center: endCoordinate,
radius: cityRadius
)
// 3
let flightPath = MKGeodesicPolyline(
coordinates: [startCoordinate, endCoordinate],
count: 2
)
// 4
view.addOverlays([startOverlay, endOverlay, flightPath])
-
First, you calculate the distance between the two locations using the
MKMapPointmethoddistance(to:), which returns the distance in meters. You then divide this value by 100. Because the map’s size is based on the distance between the cities, this value provides a consistent size for any pair of locations. -
You create two
MKCircles, one at the starting location and the other at the ending location. Both use the radius calculated in Step 1. -
To reflect the airplane’s path between the two cities, you create an
MKGeodesicPolylinebetween the starting and ending coordinates. -
You add all three overlays to the map using the
addOverlays(_:)method and passing the three overlays in an array.
If you’re familiar with MKMapView, you know you must implement the delegate method for the overlays to show. Add the following class extension after the current MapCoordinator class definition:
extension MapCoordinator: MKMapViewDelegate {
func mapView(
_ mapView: MKMapView,
rendererFor overlay: MKOverlay
) -> MKOverlayRenderer {
// 1
if overlay is MKCircle {
let renderer = MKCircleRenderer(overlay: overlay)
renderer.fillColor = UIColor.black
renderer.strokeColor = UIColor.black
return renderer
}
// 2
if overlay is MKGeodesicPolyline {
let renderer = MKPolylineRenderer(overlay: overlay)
renderer.strokeColor = UIColor(
red: 0.0,
green: 0.0,
blue: 1.0,
alpha: 0.3
)
// 3
renderer.lineWidth = 3.0
renderer.strokeStart = 0.0
renderer.strokeEnd = fraction
return renderer
}
return MKOverlayRenderer()
}
}
This extension implements the mapView(_:rendererFor:) delegate. MapKit calls this method each time it needs to render an overlay onto the map, passing in the overlay as the overlay parameter. Then:
-
In this app, you need to handle only two cases, depending on the type of overlay. This conditional uses the
iskeyword to check the type ofoverlay. When it’s of typeMKCircle, you create anMKCircleRendererobject and set the fill and stroke color to black. You end by returning theMKCircleRenderer. -
If the
overlayis of typeMKGeodesicPolyline, you similarly create anMKPolylineRendererand set the line width to narrow and the color to a mostly transparent blue. -
You use the renderer’s
strokeStartandstrokeEndproperties to define the part of the fullMKGeodesicPolylineto draw. MapKit defines the two properties as unit distances. This means you can treat the length as if the full length is 1.0. Here, you set thestrokeStartto 0.0, which begins the stroke at the starting coordinate. You set thestrokeEndto thefractionpassed into the coordinator. This ending location lets it reflect the partial distance of flights that are in progress.
Note that this class extension knows nothing about SwiftUI and would be identical to the code in a UIKit app. The Coordinator class you created handles this bridge, being called from and containing data from SwiftUI, but is able to implement the supporting methods needed by UIKit.
Now that you’ve implemented an MKMapViewDelegate, you can set it for the MKMapView. Update makeUIView(context:) to:
func makeUIView(context: Context) -> MKMapView {
let view = MKMapView(frame: .zero)
view.delegate = context.coordinator
return view
}
You now see the overlays on the preview. To use the new view, open FlightCardView.swift and add the following code at the end of the VStack:
FlightMapView(
startCoordinate: flight.startingAirportLocation,
endCoordinate: flight.endingAirportLocation,
progress: flightTimeFraction(
flight: flight
)
)
.frame(width: 300, height: 300)
Build and run the app. Tap the Flight Timeline button and you see the new timeline in action.
It doesn’t take a lot of work to integrate pre-existing Apple frameworks into your SwiftUI app. Over time, you’ll likely move more of your app’s functionality to SwiftUI when possible. The ability to integrate UIKit from your legacy apps gives you a neat way to begin using SwiftUI without having to start from scratch.