Overview

You will learn: how 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 lab 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

You must have previously installed the ArcGIS Runtime SDK for iOS and set up your development environment. Please review the install and set up instructions if you have not done this.

Make a copy of the finished Create a starter app lab (or download here) and open the .xcodeproj file in Xcode.

Complete or review labs Display point, line, and polygon graphics and Access services with OAuth 2.0 as you will repeat some parts of those labs here.

Steps

Add a method for handling user input

  1. Your app will track 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 and two AGSPoint instance variables:

    class ViewController: UIViewController {
    
        @IBOutlet weak var mapView: AGSMapView!
    
        /*** ADD ***/
        let graphicsOverlay = AGSGraphicsOverlay()
        var start: AGSPoint?
        var end: AGSPoint?
    
  2. Add new functions to ViewController to create marker symbols representing the start and end locations of the route request based on user tap gestures.

    private func addMapMarker(location: AGSPoint, style: AGSSimpleMarkerSymbolStyle, fillColor: UIColor, outlineColor: UIColor) {
        let pointSymbol = AGSSimpleMarkerSymbol(style: style, color: fillColor, size: 8)
        pointSymbol.outline = AGSSimpleLineSymbol(style: .solid, color: outlineColor, width: 2)
        let markerGraphic = AGSGraphic(geometry: location, symbol: pointSymbol, attributes: nil)
        graphicsOverlay.graphics.add(markerGraphic)
    }
    
    private func setStartMarker(location: AGSPoint) {
        graphicsOverlay.graphics.removeAllObjects()
        let startMarkerColor = UIColor(red:0.886, green:0.467, blue:0.157, alpha:1.000)
        addMapMarker(location: location, style: .diamond, fillColor: startMarkerColor, outlineColor: .blue)
        start = location
        end = nil
    }
    
    private func setEndMarker(location: AGSPoint) {
        let endMarkerColor = UIColor(red:0.157, green:0.467, blue:0.886, alpha:1.000)
        addMapMarker(location: location, style: .square, fillColor: endMarkerColor, outlineColor: .red)
        end = location
        findRoute()
    }
    
  3. Add AGSGeoViewTouchDelegate to the ViewController class declaration.

    class ViewController: UIViewController, AGSGeoViewTouchDelegate {
    
  4. In the setupMap() function, set the ViewController to be the mapView touch delegate. Also optionally update the initial viewpoint slightly to focus on an area with more roads.

    private func setupMap() {
        /*** UPDATE (optional) ***/
        mapView.map = AGSMap(basemapType: .navigationVector, latitude: 34.05293, longitude: -118.24368, levelOfDetail: 11)
    
        /*** ADD ***/
        mapView.touchDelegate = self
    }
    
  5. Define a tap event handler to call the symbol display functions you coded previously:

    func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) {
        if start == nil {
            // Start is not set, set it to a tapped location.
            setStartMarker(location: mapPoint)
        } else if end == nil {
            // End is not set, set it to the tapped location then find the route.
            setEndMarker(location: mapPoint)
        } else {
            // Both locations are set; re-set the start to the tapped location.
            setStartMarker(location: mapPoint)
        }
    }
    

Add the graphics overlay to the map view

  1. Update the setupMap() method to add the graphics overlay to the mapView:

    private func setupMap() {
        mapView.map = AGSMap(basemapType: .navigationVector, latitude: 34.05293, longitude: -118.24368, levelOfDetail: 11)
    
        mapView.touchDelegate = self
    
        /*** ADD ***/
        mapView.graphicsOverlays.add(graphicsOverlay)
    }
    

At this point you can run the app and test the display of the marker symbols on the map. Check that tapping on the map adds a point marker, and tapping a second time adds a second marker. Tapping a third time should delete both existing markers and start again by adding the first marker in the new location.

Add authentication

  1. Using the ArcGIS Transportation Routing and Network Analytics Services requires an authenticated user and a valid token. The iOS Runtime SDK handles this for you with AGSAuthenticationManager. Add a new swift file to your Xcode project named AppConfiguration.swift with the following struct, making sure the clientID, urlScheme and urlAuthPath reflect your client ID and custom redirect URL, or reuse the one created in Access services with OAuth 2.0.

    // AppConfiguration.swift
    
    struct AppConfiguration {
    
        static let clientID: String = "YOUR-APP-CLIENT-ID"
        static let urlScheme: String = "my-devlab-app"
        static let urlAuthPath: String = "auth"
        static let keychainIdentifier: String = "\(Bundle.main.bundleIdentifier!).keychainIdentifier"
    }
    
  2. Configure a redirect URL for your app. Right-click on the info.plist file in the Project Navigator and Open As > / Source Code. Edit the file just after the opening top-level <dict> tag as follows:

    <dict>
        <!-- ADD -->
        <key>CFBundleURLTypes</key>
        <array>
          <dict>
            <key>CFBundleTypeRole</key>
            <string>Editor</string>
            <key>CFBundleURLName</key>
            <!-- Your App's Bundle ID -->
            <string>com.esri.geodev.ArcGIS-access-services-with-oauth-2</string>
            <key>CFBundleURLSchemes</key>
            <array>
              <!-- Your App's Redirect URL Scheme -->
              <string>my-devlab-app</string>
            </array>
          </dict>
        </array>
    
  3. Setup the AGSAuthenticationManager in your AppDelegate.

    // AppDelegate.swift
    
    import UIKit
    /*** ADD ***/
    import ArcGIS
    
    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
    
        var window: UIWindow?
    
        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    
            /*** ADD ***/
            setupOAuthManager()
    
            return true
        }
    
        /*** ADD ***/
        private func setupOAuthManager() {
            let config = AGSOAuthConfiguration(portalURL: nil, clientID: AppConfiguration.clientID, redirectURL: "\(AppConfiguration.urlScheme)://\(AppConfiguration.urlAuthPath)")
            AGSAuthenticationManager.shared().oAuthConfigurations.add(config)
            AGSAuthenticationManager.shared().credentialCache.enableAutoSyncToKeychain(withIdentifier: AppConfiguration.keychainIdentifier, accessGroup: nil, acrossDevices: false)
        }
    
  4. Setup AppDelegate to handle a specific URL being returned from the OAuth2 login. Ensure that the inbound URL is your app's redirect URL as configured in the previous step and use the AGSAuthenticationManager to consume the inbound OAuth2 response.

    // AppDelegate.swift
    
    import UIKit
    import ArcGIS
    
    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
    
        var window: UIWindow?
    
        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    
            setupOAuthManager()
    
            return true
        }
    
        /*** ADD ***/
        func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
            if let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false),
                AppConfiguration.urlScheme == urlComponents.scheme,
                AppConfiguration.urlAuthPath == urlComponents.host {
                AGSApplicationDelegate.shared().application(app, open: url, options: options)
            }
            return true
        }
    

Add a method for building routes

  1. Routing is handled with the AGSRouteTask class. Back in ViewController.swift, create an AGSRouteTask instance variable on the ViewController class, using a routing service URL:

    let graphicsOverlay = AGSGraphicsOverlay()
    var start: AGSPoint?
    var end: AGSPoint?
    
    /*** ADD ***/
    let routeTask = AGSRouteTask(url: URL(string: "https://route.arcgis.com/arcgis/rest/services/World/Route/NAServer/Route_World")!)
    
  2. Implement the code that finds a route from the two locations set by the user. Add a new method and call it findRoute():

    func findRoute() {
        // Code from the next step goes here
    }
    
  3. In the findRoute() function, asynchronously ask for the default parameters:

    routeTask.defaultRouteParameters { [weak self] (defaultParameters, error) in
        guard error == nil else {
            print("Error getting default parameters: \(error!.localizedDescription)")
            return
        }
    
        // Code from the next step goes here
    }
    
  4. Configure the parameters by setting the stops to route between, and call solveRoute():

    guard let params = defaultParameters, let self = self, let start = self.start, let end = self.end else { return }
    
    params.setStops([AGSStop(point: start), AGSStop(point: end)])
    
    self.routeTask.solveRoute(with: params, completion: { (result, error) in
        guard error == nil else {
            print("Error solving route: \(error!.localizedDescription)")
            return
        }
    
        // Code from the next step goes here
    })
    

Add code to draw the route on the map

  1. Create an AGSPolyline and AGSSimpleLineSymbol and use them to define a new AGSGraphic. Add the graphic to the graphicsOverlay:

    if let firstRoute = result?.routes.first, let routePolyline = firstRoute.routeGeometry {
        let routeSymbol = AGSSimpleLineSymbol(style: .solid, color: .blue, width: 4)
        let routeGraphic = AGSGraphic(geometry: routePolyline, symbol: routeSymbol, attributes: nil)
        self.graphicsOverlay.graphics.add(routeGraphic)
    }
    
  2. Uncomment the call to findRoute() in the setEndMarker() method in step 2.

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, called firstRoute in this example, has additional information accessible through a series of properties. Add the following lines of code to findRoute() after getting firstRoute:

let totalDistance = Measurement(value: firstRoute.totalLength, unit: UnitLength.meters)
let totalDuration = Measurement(value: firstRoute.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 lab you handled the first result but ignored any additional results. Can you create a UI to allow the user to review multiple results?