Skip To Content ArcGIS for Developers Sign In Dashboard

Overview

You will learn: how to build an app to find the optimal route and directions for multiple stops with the ArcGIS Route service.

The ArcGIS Transportation Routing and Network Analytics Services can find routes, get driving directions, calculate drive times, and solve complicated multiple vehicle routing problems (VRP). If you would like to create an application that can find driving directions and create an optimized route, you can use a route task and the solveRouteWithParameters:completion:() method. You pass in the "stop" locations, and the service will return a route with directions. Once you have the results you can add the route to a map, display the turn-by-turn directions, or integrate them further into your application. To learn more about the capabilities of the directions and routing service, please visit the documentation.

In this tutorial you will learn how to use a routing service to calculate an optimal route between two or more places. Points will be selected by user taps on the map.

Before you begin

Make sure you have installed the latest version of Xcode.

Reuse the starter project

If you have completed the Create a starter app tutorial, then copy the project into a new empty folder. Otherwise, download and unzip the project solution. Open the .xcodeproj file in Xcode. Run and verify the map displays in the device simulator.

Complete or review Display point, line, and polygon graphics as you will repeat some parts of the tutorial here.

Register a new app and repeat the steps you followed in Access services with OAuth 2.0.

Steps

Set the app settings

  1. The app will use the ArcGIS World Routing service to generate routes. Update the AppConfiguration.swift file with a new URL.

    extension URL {
        /** UPDATE **/
        static let worldRoutingService = URL(string: "https://route.arcgis.com/arcgis/rest/services/World/Route/NAServer/Route_World")!
    }
    

Create route graphics

  1. Your app will display the start and end points for the route and display the route using an AGSGraphicsOverlay. In the ViewController.swift file, create an AGSGraphicsOverlay instance variable.

    private let routeGraphics = AGSGraphicsOverlay()
    
  2. Your app will use three instances of AGSGraphic to display the start point, end point, and route polyline. Create three AGSGraphic instance variables with no geometry.

    private lazy var startGraphic: AGSGraphic = {
        let symbol = AGSSimpleMarkerSymbol(style: .diamond, color: .orange, size: 8)
        symbol.outline = AGSSimpleLineSymbol(style: .solid, color: .blue, width: 2)
        let graphic = AGSGraphic(geometry: nil, symbol: symbol)
        return graphic
    }()
    
    private lazy var endGraphic: AGSGraphic = {
        let symbol = AGSSimpleMarkerSymbol(style: .square, color: .green, size: 8)
        symbol.outline = AGSSimpleLineSymbol(style: .solid, color: .red, width: 2)
        let graphic = AGSGraphic(geometry: nil, symbol: symbol)
        return graphic
    }()
    
    private lazy var routeGraphic: AGSGraphic = {
        let symbol = AGSSimpleLineSymbol(style: .solid, color: .blue, width: 4)
        let graphic = AGSGraphic(geometry: nil, symbol: symbol)
        return graphic
    }()
    
  3. Add a new function that adds the three graphics to the graphics overlay and adds the graphics overlay to the map.

    private func addGraphics() {
        mapView.graphicsOverlays.add(routeGraphics)
        routeGraphics.graphics.addObjects(from: [routeGraphic, startGraphic, endGraphic])
    }
    
  4. Update viewDidLoad() to call the new addGraphics() method.

    /** UPDATE **/
    override func viewDidLoad() {
        super.viewDidLoad()
        setupMap()
        /** ADD **/
        addGraphics()
    }
    

Create a route builder

  1. Your app will reflect each stage of collecting route parameters and solving the route as a unique state. Create an enum named RouteBuilderStatus and establish four cases.

    private enum RouteBuilderStatus {
        case none
        case selectedStart(AGSPoint)
        case selectedStartAndEnd(AGSPoint, AGSPoint)
        case routeSolved(AGSPoint, AGSPoint, AGSPolyline)
    }
    
  2. Create an instance of RouteBuilderStatus to persist the state of the route building process. A didSet callback is used to update the geometry of route graphics, reflecting state. Note that setting a graphic's geometry to nil will remove that graphic from the map.

    private var status: RouteBuilderStatus = .none {
        didSet {
            switch status {
            case .none:
                startGraphic.geometry = nil
                endGraphic.geometry = nil
                routeGraphic.geometry = nil
            case .selectedStart(let start):
                startGraphic.geometry = start
                endGraphic.geometry = nil
                routeGraphic.geometry = nil
            case .selectedStartAndEnd(let start, let end):
                startGraphic.geometry = start
                endGraphic.geometry = end
                routeGraphic.geometry = nil
            case .routeSolved(let start, let end, let route):
                startGraphic.geometry = start
                endGraphic.geometry = end
                routeGraphic.geometry = route
            }
        }
    }
    

Solve a route

  1. Routing is handled with the AGSRouteTask class. Create an AGSRouteTask instance variable on the ViewController class, using a routing service URL.

    /*** ADD ***/
    private let routeTask = AGSRouteTask(url: .worldRoutingService)
    
  2. Add a currentSolveRouteOperation member variable to ViewController to track the current route task. This cancelable reference is used to cancel an existing running routing operation if a new one is performed instead.

    /** ADD **/
    private var currentSolveRouteOperation: AGSCancelable?
    
  3. Add a new method named solveRoute that takes two point parameters and offers a completion closure with solved routes or an error. This method should first cancel any existing solve route operation, if there is one.

    /** ADD **/
    private func solveRoute(start: AGSPoint, end: AGSPoint, completion: @escaping (Result<[AGSRoute], Error>) -> Void) {
        currentSolveRouteOperation?.cancel()
    }
    
  4. Generate default route parameters for the route task. The default route parameters serve the most common use-case for the service. If the generate operation failed, finish the method early.

    private func solveRoute(start: AGSPoint, end: AGSPoint, completion: @escaping (Result<[AGSRoute], Error>) -> Void) {
        currentSolveRouteOperation?.cancel()
        /** ADD **/
        currentSolveRouteOperation = routeTask.defaultRouteParameters { [weak self] (defaultParameters, error) in
            guard let self = self else { return }
    
            if let error = error {
                completion(.failure(error))
                return
            }
        }
    }
    
  5. Modify the default route parameters by setting the start and stop points. Finish the method by solving the route and calling the completion callback with the results.

    private func solveRoute(start: AGSPoint, end: AGSPoint, completion: @escaping (Result<[AGSRoute], Error>) -> Void) {
    
        currentSolveRouteOperation?.cancel()
    
        currentSolveRouteOperation = routeTask.defaultRouteParameters { [weak self] (defaultParameters, error) in
            guard let self = self else { return }
    
            if let error = error {
                completion(.failure(error))
                return
            }
    
            /** ADD **/
            guard let params = defaultParameters else { return }
            params.setStops([AGSStop(point: start), AGSStop(point: end)])
    
            self.currentSolveRouteOperation = self.routeTask.solveRoute(with: params) { (routeResult, error) in
    
                if let routes = routeResult?.routes {
                    completion(.success(routes))
                }
                else if let error = error {
                    completion(.failure(error))
                }
            }
        }
    }
    

Respond to map interactions

  1. Update the setupMap method by establishing ViewController as the map view's touch delegate (AGSGeoViewTouchDelegate). This will allow the view controller to respond when a user touches the map view.

    /** UPDATE **/
    private func setupMap() {
        mapView.map = AGSMap(
            basemapType: .navigationVector,
            latitude: 34.05293,
            longitude: -118.24368,
            levelOfDetail: 11
        )
        /** ADD **/
        mapView.touchDelegate = self
    }
    
  2. Extend ViewController to adopt the AGSGeoViewTouchDelegate protocol. Implement the AGSGeoViewTouchDelegate method for responding to tap events. Cancel the actively running route task, if there is one.

    /** ADD **/
    extension ViewController: AGSGeoViewTouchDelegate {
    
        func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) {
    
            currentSolveRouteOperation?.cancel()
        }
    }
    
  3. Using the app's current state, receive the touch event and modify the app's state to reflect the next step of the route building process.

    extension ViewController: AGSGeoViewTouchDelegate {
    
        func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) {
    
            currentSolveRouteOperation?.cancel()
    
            /** ADD **/
            switch status {
            case .none:
                status = .selectedStart(mapPoint)
            case .selectedStart(let start):
                status = .selectedStartAndEnd(start, mapPoint)
            case .selectedStartAndEnd(_, _):
                status = .selectedStart(mapPoint)
            case .routeSolved(_, _, _):
                status = .selectedStart(mapPoint)
            }
        }
    }
    
  4. If the user has selected a start and end point, solve the route and show the route graphic to the map.

    extension ViewController: AGSGeoViewTouchDelegate {
    
        func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) {
    
            currentSolveRouteOperation?.cancel()
    
            switch status {
            case .none:
                status = .selectedStart(mapPoint)
            case .selectedStart(let start):
                status = .selectedStartAndEnd(start, mapPoint)
            case .selectedStartAndEnd(_, _):
                status = .selectedStart(mapPoint)
            case .routeSolved(_, _, _):
                status = .selectedStart(mapPoint)
            }
    
            /** ADD **/
            if case let .selectedStartAndEnd(start, end) = status {
    
                solveRoute(start: start, end: end) { [weak self] (result) in
                    guard let self = self else { return }
    
                    switch result {
                    case .failure(let error):
                        print(error.localizedDescription)
                        self.status = .none
                    case .success(let routes):
                        if let line = routes.first?.routeGeometry {
                            self.status = .routeSolved(start, end, line)
                        }
                        else {
                            self.status = .none
                        }
                    }
                }
            }
        }
    }
    
  5. Run the app. Press Command-R to run the app in the iOS Simulator.

    (Note, as of 100.8 Runtime supports Metal. In order to run your app in a simulator you must meet some minimum requirements. You must be developing on macOS Catalina, using Xcode 11, and simulating iOS 13. If you do not meet these requirements, you should run the app on a physical iOS device.)

Congratulations, you're done!

Your map should now display a route between two tapped points. The first time you run the app, you will be redirected to log in with your ArcGIS Online credentials. Compare with our completed solution project.

Challenge

There are several ways this app could be enhanced. Can you take it to the next level?

Explore route parameters

The AGSRouteParameters class offers a lot of options. Explore some of these options and enhance your app. Can you show turn-by-turn directions?

Get additional information about the route

The solution route (the first route provided in the results) in this example, has additional information accessible through a series of properties. Update the following lines of code to solveRoute() after getting the solution route:

/** UPDATE **/
if let route = routes.first, let line = route.routeGeometry {
    self.status = .routeSolved(start, end, line)
    let totalDistance = Measurement(value: route.totalLength, unit: UnitLength.meters)
    let totalDuration = Measurement(value: route.travelTime, unit: UnitDuration.minutes)

    let formatter = MeasurementFormatter()
    formatter.numberFormatter.maximumFractionDigits = 2
    formatter.unitOptions = .naturalScale

    let alert = UIAlertController(title: nil, message: """
        Total distance: \(formatter.string(from: totalDistance))
        Travel time: \(formatter.string(from: totalDuration))
        """, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
    self.present(alert, animated: true, completion: nil)
}

Search and directions

Combine what you learned in Search for an address to allow your user to search for the start and end locations.

Handle multiple results

In this tutorial you handled the first result but ignored any additional results. Can you create a UI to allow the user to review multiple results?