Building Engaging User Interfaces with SwiftUI

Mar 12 2025 · Swift 5.9, iOS 17.0, XCode 15.0

Lesson 03: Integrating SwiftUI with UIKit

Building a Coordinator Class

Episode complete

Play next episode

Next
Transcript

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])
  1. First, you calculate the distance between the two locations using the MKMapPoint method distance(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.
  2. You create two MKCircles, one at the starting location and the other at the ending location. Both use the radius calculated in Step 1.
  3. To reflect the airplane’s path between the two cities, you create an MKGeodesicPolyline between the starting and ending coordinates.
  4. 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:

  1. In this app, you need to handle only two cases, depending on the type of overlay. This conditional uses the is keyword to check the type of overlay. When it’s of type MKCircle, you create an MKCircleRenderer object and set the fill and stroke color to black. You end by returning the MKCircleRenderer.
  2. If the overlay is of type MKGeodesicPolyline, you similarly create an MKPolylineRenderer and set the line width to narrow and the color to a mostly transparent blue.
  3. You use the renderer’s strokeStart and strokeEnd properties to define the part of the full MKGeodesicPolyline to 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 the strokeStart to 0.0, which begins the stroke at the starting coordinate. You set the strokeEnd to the fraction passed 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.

See forum comments
Cinema mode Download course materials from Github
Previous: Integrating SwiftUI with UIKit Next: Conclusion