Map in SwiftUI (Episode V)

In this fifth and final episode of our series on Maps in SwiftUI, we’ll learn how to:

  1. Draw the route
  2. Calculate the travel time
  3. Change the transport type

As usual, let’s start from the beginning.

In the code, we set the starting point at Infinite Loop and the ending point at Apple Park. By clicking on the car icon, we change the transport type to walking.

Now, let’s begin analyzing the declarations in the view:

struct RouteView: View {
    @State private var route: MKRoute?
    @State private var formattedTravelTime: String = ""
    @State private var transportType: MKDirectionsTransportType = .automobile
    private let infiniteLoop = CLLocationCoordinate2D(latitude: 37.33218745278164, longitude: -122.03010316931332)
    private let applePark = CLLocationCoordinate2D(latitude: 37.33536036460403, longitude: -122.00901491534331)
    private let images = [MKDirectionsTransportType.automobile.rawValue: "car", MKDirectionsTransportType.walking.rawValue: "figure.walk"]
.
.
.

The route will contain the polyline of the route, while formattedTravelTime holds the time needed to complete the journey. transportType contains the mode of transportation used for the journey (in this example, we use only automobile and walking). infiniteLoop and applePark are the two points of interest that we want to connect. The images variable contains a dictionary with two elements, each representing the image name used for the transport type.

var body: some View {
        Map {
            if let route {
                MapPolyline(route.polyline)
                    .stroke(.blue, lineWidth: 8)
            }
        }
        .overlay(alignment: .bottom, content: {
            HStack {
                Text("Travel time: \(formattedTravelTime)")
                    .font(.headline)
                    .foregroundStyle(.black)
                    .fontWeight(.bold)
                    
                Button(action: {
                    if transportType == .automobile {
                        transportType = .walking
                    } else {
                        transportType = .automobile
                    }
                    fetchRouteFrom(infiniteLoop, to: applePark)
                }, label: {
                    Image(systemName: images[transportType.rawValue] ?? "questionmark.app")
                })
            }.padding()
            .background(.ultraThinMaterial)
            .cornerRadius(15)
        })
        .onAppear(perform: {
            fetchRouteFrom(infiniteLoop, to: applePark)
        })
    }

With this code, we display a map and, if not nil, the route as a polyline. The route is calculated by the fetchRouteFrom function. This function is called when the view is displayed and every time we tap on the transport type button. Next to the button, the required travel time is also displayed.

Now, let’s take a look at the functions used:

private func fetchRouteFrom(_ source: CLLocationCoordinate2D, to destination: CLLocationCoordinate2D) {
        let request = MKDirections.Request()
        request.source = MKMapItem(placemark: MKPlacemark(coordinate: source))
        request.destination = MKMapItem(placemark: MKPlacemark(coordinate: destination))
        request.transportType = transportType
        
        Task {
            let result = try? await MKDirections(request: request).calculate()
            route = result?.routes.first
            if let time = travelTime() {
                formattedTravelTime = time
            } else {
                formattedTravelTime = "unknown"
            }
            
        }
    }

These functions receive the coordinates of the two points as parameters. A request to MKDirections is created, setting the placemarks for the source and destination, as well as the transport type.

Following this, we calculate the route, retrieve the first available route, and then calculate the travel time with:

private func travelTime() -> String? {
        guard let route else { return nil}
        let formatter = DateComponentsFormatter()
        formatter.unitsStyle = .abbreviated
        formatter.allowedUnits = [.hour, .minute]
        return formatter.string(from: route.expectedTravelTime)
    }

In this function, we obtain the travel time using route.expectedTravelTime and format it with a formatter.

Now, let’s put everything together:

import SwiftUI
import MapKit

struct ContentView: View {
    @State private var route: MKRoute?
    @State private var formattedTravelTime: String = ""
    @State private var transportType: MKDirectionsTransportType = .automobile
    private let infiniteLoop = CLLocationCoordinate2D(latitude: 37.33218745278164, longitude: -122.03010316931332)
    private let applePark = CLLocationCoordinate2D(latitude: 37.33536036460403, longitude: -122.00901491534331)
    private let images = [MKDirectionsTransportType.automobile.rawValue: "car", MKDirectionsTransportType.walking.rawValue: "figure.walk"]
    
    var body: some View {
        Map {
            if let route {
                MapPolyline(route.polyline)
                    .stroke(.blue, lineWidth: 8)
            }
        }
        .overlay(alignment: .bottom, content: {
            HStack {
                Text("Travel time: \(formattedTravelTime)")
                    .font(.headline)
                    .foregroundStyle(.black)
                    .fontWeight(.bold)
                    
                Button(action: {
                    if transportType == .automobile {
                        transportType = .walking
                    } else {
                        transportType = .automobile
                    }
                    fetchRouteFrom(infiniteLoop, to: applePark)
                }, label: {
                    Image(systemName: images[transportType.rawValue] ?? "questionmark.app")
                })
            }.padding()
            .background(.ultraThinMaterial)
            .cornerRadius(15)
        })
        .onAppear(perform: {
            fetchRouteFrom(infiniteLoop, to: applePark)
        })
    }
    private func fetchRouteFrom(_ source: CLLocationCoordinate2D, to destination: CLLocationCoordinate2D) {
        let request = MKDirections.Request()
        request.source = MKMapItem(placemark: MKPlacemark(coordinate: source))
        request.destination = MKMapItem(placemark: MKPlacemark(coordinate: destination))
        request.transportType = transportType
        
        Task {
            let result = try? await MKDirections(request: request).calculate()
            route = result?.routes.first
            if let time = travelTime() {
                formattedTravelTime = time
            } else {
                formattedTravelTime = "unknown"
            }
            
        }
    }
        
    private func travelTime() -> String? {
        guard let route else { return nil}
        let formatter = DateComponentsFormatter()
        formatter.unitsStyle = .abbreviated
        formatter.allowedUnits = [.hour, .minute]
        return formatter.string(from: route.expectedTravelTime)
       
    }
}

With this episode, our series on maps is now complete.

Stay tuned and follow me for upcoming topics.

share this post with friends

Picture of Nicola De filippo

Nicola De filippo

I'm a software engineer who adds to the passion for technologies the wisdom and the experience without losing the wonder for the world. I love to create new projects and to help people and teams to improve

Leave a comment

Your email address will not be published. Required fields are marked *

Who I am

I'm a software engineer who adds to the passion for technologies the wisdom and the experience without losing the wonder for the world. I love to create new projects and to help people and teams to improve.

Follow Me Here

Get The Latest Updates

Periodically receive my super contents on coding and programming

join the family;)

Recent Posts