Learn how to find a route and directions with the route service.
Routing is the process of finding the path from an origin to a destination in a street network. You can use the Routing service to find routes, get driving directions, calculate drive times, and solve complicated, multiple vehicle routing problems. To create a route, you typically define a set of stops (origin and one or more destinations) and use the service to find a route with directions. You can also use a number of additional parameters such as barriers and mode of travel to refine the results.
In this tutorial, you define an origin and destination by clicking on the map. These values are used to get a route and directions from the route service. The directions are also displayed on the map.
Prerequisites
The following are required for this tutorial:
- An ArcGIS account to access your API keys. If you don't have an account, sign up for free.
- Your system meets the system requirements.
Steps
Open the Xcode project
-
To start the tutorial, complete the Display a map tutorial or download and unzip the solution.
-
Open the
.xcodeproj
file in Xcode. -
If you downloaded the solution project, set your API key.
An API Key enables access to services, web maps, and web scenes hosted in ArcGIS Online.
-
Go to your developer dashboard to get your API key. For these tutorials, use your default API key. It is scoped to include all of the services demonstrated in the tutorials.
- In Xcode, in the Project Navigator, click AppDelegate.swift.
- In the editor, set the
APIKey
property on theAGSArcGISRuntime
with your API key.Environment
AppDelegate.swiftUse dark colors for code blocks func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Note: it is not best practice to store API keys in source code. // The API key is referenced here for the convenience of this tutorial. AGSArcGISRuntimeEnvironment.apiKey = "YOUR_API_KEY" return true }
-
Update the map
A navigation basemap layer is typically used in routing applications. Update the basemap to use the .arc
basemap style, and change the position of the map to center on Los Angeles.
-
Update the
AGSBasemap
style property from.arc
toGISTopographic .arc
and update the latitude and longitude coordinates to center on Los Angeles.GISNavigation ViewController.swiftUse dark colors for code blocks Change line Change line Change line Change line Change line Change line Change line Change line private func setupMap() { mapView.map = AGSMap(basemapStyle: .arcGISNavigation) mapView.setViewpoint( AGSViewpoint( latitude: 34.05293, longitude: -118.24368, scale: 288_895 ) ) }
Receive map view touch events
The app will use locations derived from a user tapping the map view to generate the stops of a route. Conform the view controller to receive touch events from the map view. The locations derived from a user tapping the map view will be used to generate routes in a later step.
-
In Xcode, in the Project Navigator, click ViewController.swift.
-
In the editor, extend
View
to conform to theController AGSGeo
protocol and include theView Touch Delegate geo
geoview touch delegate method.View: did T a p At Screen Point: map Point: () ViewController.swiftUse dark colors for code blocks Add line. Add line. Add line. Add line. Add line. Add line. Add line. // MARK: - GeoView Touch Delegate extension ViewController: AGSGeoViewTouchDelegate { func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { } }
-
In the
setup
method, assignMap() View
toController map
.View.delegate
This step bridges map
user touch interactions with View
via the AGSGeo
protocol.
private func setupMap() {
mapView.touchDelegate = self
mapView.map = AGSMap(basemapStyle: .arcGISNavigation)
mapView.setViewpoint(
AGSViewpoint(
latitude: 34.05293,
longitude: -118.24368,
scale: 288_895
)
)
}
Add graphics to the map view
A graphics overlay is a container for graphics. Graphics are added as a visual means to display the search result on the map.
-
Create a private
AGSGraphic
property namedstart
. This graphic will be used to display the route's start location.Graphic An
AGSSimple
is used to display a location on the map view.Marker Symbol ViewController.swiftUse dark colors for code blocks Add line. Add line. Add line. Add line. Add line. Add line. // MARK: - Route Graphics private let startGraphic: AGSGraphic = { let symbol = AGSSimpleMarkerSymbol(style: .circle, color: .white, size: 8) symbol.outline = AGSSimpleLineSymbol(style: .solid, color: .black, width: 1) let graphic = AGSGraphic(geometry: nil, symbol: symbol) return graphic }()
-
Create a private
AGSGraphic
property namedend
. This graphic will be used to display the route's end location.Graphic An
AGSSimple
is used to display a location on the map view.Marker Symbol ViewController.swiftUse dark colors for code blocks Add line. Add line. Add line. Add line. Add line. Add line. // MARK: - Route Graphics private let startGraphic: AGSGraphic = { let symbol = AGSSimpleMarkerSymbol(style: .circle, color: .white, size: 8) symbol.outline = AGSSimpleLineSymbol(style: .solid, color: .black, width: 1) let graphic = AGSGraphic(geometry: nil, symbol: symbol) return graphic }() private let endGraphic: AGSGraphic = { let symbol = AGSSimpleMarkerSymbol(style: .circle, color: .black, size: 8) symbol.outline = AGSSimpleLineSymbol(style: .solid, color: .black, width: 1) let graphic = AGSGraphic(geometry: nil, symbol: symbol) return graphic }()
-
Create a private
AGSGraphic
property namedroute
. This graphic will be used to display the route line.Graphic An
AGSSimple
is used to display a line on the map view.Line Symbol ViewController.swiftUse dark colors for code blocks Add line. Add line. Add line. Add line. Add line. private let endGraphic: AGSGraphic = { let symbol = AGSSimpleMarkerSymbol(style: .circle, color: .black, size: 8) symbol.outline = AGSSimpleLineSymbol(style: .solid, color: .black, width: 1) let graphic = AGSGraphic(geometry: nil, symbol: symbol) return graphic }() private let routeGraphic: AGSGraphic = { let symbol = AGSSimpleLineSymbol(style: .solid, color: .blue, width: 3) let graphic = AGSGraphic(geometry: nil, symbol: symbol) return graphic }()
-
Define a private method named
add
. InGraphics() add
create anGraphics() AGSGraphics
. AppendOverlay start
,Graphic end
, andGraphic route
to the graphics overlay and add the graphics overlay to the map view.Graphic Because
start
,Graphic end
, andGraphic route
haven't yet specified a geometry, they will not be visible.Graphic ViewController.swiftUse dark colors for code blocks Add line. Add line. Add line. Add line. Add line. private let routeGraphic: AGSGraphic = { let symbol = AGSSimpleLineSymbol(style: .solid, color: .blue, width: 3) let graphic = AGSGraphic(geometry: nil, symbol: symbol) return graphic }() private func addGraphics() { let routeGraphics = AGSGraphicsOverlay() mapView.graphicsOverlays.add(routeGraphics) routeGraphics.graphics.addObjects(from: [routeGraphic, startGraphic, endGraphic]) }
-
In the
view
method, callD i d Load() add
.Graphics() ViewController.swiftUse dark colors for code blocks Add line. override func viewDidLoad() { super.viewDidLoad() setupMap() addGraphics() }
Define app route builder status
The app will leverage a state-machine design pattern to ensure the contents of the map reflect the state of gathering the route parameters and generating the route result. This design pattern supports wrangling multiple asynchronous requests to the world routing service into a single state variable result, ensuring the reliability of route results.
-
Define an enum named
Route
with four cases. These four cases are used to gather route parameters and display the route's result, over time.Builder Status ViewController.swiftUse dark colors for code blocks Add line. Add line. Add line. Add line. Add line. Add line. Add line. // MARK: - Route Builder private enum RouteBuilderStatus { case none case selectedStart(AGSPoint) case selectedStartAndEnd(AGSPoint, AGSPoint) case routeSolved(AGSPoint, AGSPoint, AGSRoute) }
-
Define a method of
Route
namedBuilder Status next
. This method is used to step through generating a route, over time.Status With: point() ViewController.swiftUse dark colors for code blocks Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. // MARK: - Route Builder private enum RouteBuilderStatus { case none case selectedStart(AGSPoint) case selectedStartAndEnd(AGSPoint, AGSPoint) case routeSolved(AGSPoint, AGSPoint, AGSRoute) func nextStatus(with point: AGSPoint) -> RouteBuilderStatus { switch self { case .none: return .selectedStart(point) case .selectedStart(let start): return .selectedStartAndEnd(start, point) case .selectedStartAndEnd: return .selectedStart(point) case .routeSolved: return .selectedStart(point) } } }
-
Create a private
Route
property namedBuilder Status status
. Set the default value.none
.This property maintains the status of route parameters as they are gathered and the route is generated, over time. Updating the status will update the geometry of route graphics and display them in the map view.
ViewController.swiftUse dark colors for code blocks Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. 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.routeGeometry } } }
Add a UI to display driving directions
To display the turn-by-turn directions from the route, a UI element is required.
-
Create a private lazy
UIBar
property namedButton Item directions
. Lazy loading the bar button item guaranteesButton self
is created prior to assigning the button's target.ViewController.swiftUse dark colors for code blocks Add line. Add line. Add line. Add line. Add line. // MARK: - Directions Button private lazy var directionsButton: UIBarButtonItem = { let button = UIBarButtonItem(title: "Show Directions", style: .plain, target: self, action: #selector(displayDirections)) button.isEnabled = false return button }()
-
Define a method named
display
and assign it theDirections: () @objc
keyword. The@objc
method keyword exposes the method to Objective-C, a necessary step for using theUIBar
API.Button Item This method collates the route maneuvers and presents the directions to the user in an alert.
ViewController.swiftUse dark colors for code blocks Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. // MARK: - Directions Button private lazy var directionsButton: UIBarButtonItem = { let button = UIBarButtonItem(title: "Show Directions", style: .plain, target: self, action: #selector(displayDirections)) button.isEnabled = false return button }() @objc func displayDirections(_ sender: AnyObject) { guard case let .routeSolved(_, _, route) = status else { return } let directions = route.directionManeuvers.enumerated() .reduce(into: "\n") { $0 += "\($1.offset + 1). \($1.element.directionText).\n\n" } let alert = UIAlertController(title: "Directions", message: directions, preferredStyle: .alert) let okay = UIAlertAction(title: "Hide Directions", style: .default, handler: nil) alert.addAction(okay) present(alert, animated: true, completion: nil) }
-
In the
status
property'sdid
closure, update each state to enable or disable theSet directions
. The directions button should be enabled only if the route is solved.Button ViewController.swiftUse dark colors for code blocks Add line. Add line. Add line. Add line. private var status: RouteBuilderStatus = .none { didSet { switch status { case .none: startGraphic.geometry = nil endGraphic.geometry = nil routeGraphic.geometry = nil directionsButton.isEnabled = false case .selectedStart(let start): startGraphic.geometry = start endGraphic.geometry = nil routeGraphic.geometry = nil directionsButton.isEnabled = false case .selectedStartAndEnd(let start, let end): startGraphic.geometry = start endGraphic.geometry = end routeGraphic.geometry = nil directionsButton.isEnabled = false case .routeSolved(let start, let end, let route): startGraphic.geometry = start endGraphic.geometry = end routeGraphic.geometry = route.routeGeometry directionsButton.isEnabled = true } } }
-
In
view
, set theD i d Load() navigation
toItem.right B a r Button Item directions
.Button ViewController.swiftUse dark colors for code blocks Add line. override func viewDidLoad() { super.viewDidLoad() setupMap() addGraphics() navigationItem.rightBarButtonItem = directionsButton }
-
In Xcode, in the Project Navigator, click Main.storyboard.
-
In the editor, select
View
. In the menu bar, click Editor > Embed In > Navigation Controller.Controller Embedding
View
within a Navigation Controller will place a navigation bar at the top ofController View
. Inside the navigation bar you will find the directions bar button item.Controller
Create a route task and route parameters
A task makes a request to a service and returns the results. Use the AGSRoute
class to access a routing service.A routing service with global coverage is part of ArcGIS location services. You can also publish custom routing services using ArcGIS Enterprise.
-
In Xcode, in the Project Navigator, click ViewController.swift.
-
In the editor, create a private
AGSRoute
property namedTask route
with the routing service.Task ViewController.swiftUse dark colors for code blocks Add line. Add line. Add line. Add line. // MARK: - Route Task private let routeTask: AGSRouteTask = { let worldRoutingService = URL(string: "https://route-api.arcgis.com/arcgis/rest/services/World/Route/NAServer/Route_World")! return AGSRouteTask(url: worldRoutingService) }()
-
Create a private optional
AGSCancelable
property namedcurrent
.Solve Route Operation This property will maintain a reference to the route task operation in case a user submits a second query and the operation should be canceled.
ViewController.swiftUse dark colors for code blocks Add line. private let routeTask: AGSRouteTask = { let worldRoutingService = URL(string: "https://route-api.arcgis.com/arcgis/rest/services/World/Route/NAServer/Route_World")! return AGSRouteTask(url: worldRoutingService) }() private var currentSolveRouteOperation: AGSCancelable?
-
Define a private method named
solve
and cancel the current route task operation, if one exists.Route Start: end: completion: () ViewController.swiftUse dark colors for code blocks Add line. Add line. Add line. Add line. Add line. private func solveRoute(start: AGSPoint, end: AGSPoint, completion: @escaping (Result<[AGSRoute], Error>) -> Void) { currentSolveRouteOperation?.cancel() }
-
Get default
AGSRoute
from theParameters route
.Task The
default
method is asynchronous and you must handle its completion and check for errors.Route Parameters With Completion: () ViewController.swiftUse dark colors for code blocks Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. 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 } } }
-
Modify the default parameters with start and end route stops provided by the user.
ViewController.swiftUse dark colors for code blocks Add line. Add line. Add line. 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 } guard let params = defaultParameters else { return } params.returnDirections = true params.setStops([AGSStop(point: start), AGSStop(point: end)]) } }
-
Perform the
solve
method and supply the parameters containing the route stops.Route With Parameters: completion: () ViewController.swiftUse dark colors for code blocks Add line. Add line. Add line. Add line. Add line. Add line. Add line. 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 } guard let params = defaultParameters else { return } params.returnDirections = true 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)) } } } }
Generate a route from user tap gestures
A user's tap gestures are used to generate a route. The geo view touch delegate forwards messages from the map view to View
, allowing the app to decide what step to take next.
-
Find the
View
extension ofController AGSGeo
and theView Touch Delegate geo
method.View: did T a p At Screen Point: map Point: () -
Cancel the current solve route operation, if one exists.
ViewController.swiftUse dark colors for code blocks Add line. Add line. extension ViewController: AGSGeoViewTouchDelegate { func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { currentSolveRouteOperation?.cancel() } }
-
Set the next status using the
Route
's helper method,Builder Status next
.Status With: point() ViewController.swiftUse dark colors for code blocks Add line. func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { currentSolveRouteOperation?.cancel() status = status.nextStatus(with: mapPoint) }
-
If start and end locations are gathered, perform
solve
. The result is used to updateRoute: start: end() status
and draw the route's line on the map.ViewController.swiftUse dark colors for code blocks Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { currentSolveRouteOperation?.cancel() status = status.nextStatus(with: mapPoint) 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 route = routes.first { self.status = .routeSolved(start, end, route) } else { self.status = .none } } } } }
-
Press Command + R to run the app.
If you are using the Xcode simulator your system must meet these minimum requirements: macOS Big Sur 11.3, Xcode 13, iOS 13. If you are using a physical device, then refer to the system requirements.
The map should support two taps to create an origin and destination point and then use the route service to display the resulting route.
What's next?
Learn how to use additional API features, ArcGIS location services, and ArcGIS tools in these tutorials: