THIS SDK ISDEPRECATED.

We rolled out a new and better SDK for you.

Time to leave

Purpose

This tutorial shows you how to use the TomTom Maps SDK for iOS to create an application that calculates the time to leave before proceeding to the destination point at the desired time.

Getting Started

A user can plan their route to reach the destination using the following items:

  • Departure and destination locations.
  • Travel mode.
  • The time they want to reach the destination.
  • Optionally preparation time before the departure.

How it works:

  1. The route is calculated and displayed on a map.
  2. The time to leave timer starts to count down.
  3. The route is recalculated each minute to check for any traffic delays.
  4. In case of any changes in departure time, the countdown timer is updated and the user is notified.

This article does not cover the whole of the application’s code description, only the code sections where the TomTom SDK is used.

The application is written in Swift language. If you are interested in Objective-C language, please check the Search along a route application.

You can find and clone the working application on our GitHub. Have fun with modifying it and testing our APIs! If you want to follow a step-by-step application creation from scratch, check out the “Search Along the Route” tutorial.

The following sections describe the TomTom:

  • Search SDK module to search for addresses
  • Map SDK module to display a map
  • Routing SDK module to plan routes

Prerequisites:

  1. Clone a GitHub repository, with the source code of the TimeToLeave application:
  2. If you don't have an API key visit a How to get a TomTom API key site and create one.
  3. Copy and paste your API key into a key placeholders inside the Keys.swift file.
    1import Foundation
    2public class Key: NSObject {
    3 @objc public static let Map = "Map.Key"
    4 @objc public static let Routing = "Routing.Key"
    5 @objc public static let Search = "Search.Key"
    6 @objc public static let Traffic = "Traffic.Key"
    7}
  4. Run pod install command open a newly created *.xcworkspace file. For more details about this step, please see the downloads section.

How it works

Results from the Search API query are used to feed departure and destination autocomplete text fields with the list of suggested positions.

  1. The TomTomOnlineSDKSearch package is imported on top of the file so that the tomtomSearchAPI object can be defined inside the MainViewController class:

    import TomTomOnlineSDKSearch
    let tomtomSearchAPI = TTSearch(key: Key.Search)
  2. The MainViewController class conforms to the TTSearchDelegate protocol and assigns itself as a delegate in the tomtomSearchAPI object:

    1class MainViewController: UIViewController, TTSearchDelegate {
    2 override func viewDidLoad() {
    3 super.viewDidLoad()
    4 self.tomtomSearchAPI.delegate = self
    5 }
  3. Searching is triggered inside the editingChanged method every time when editing of the departure or destination location has changed and has more than three characters.

    1@IBAction func editingChanged(_ textView: UITextField) {
    2 let enteredText = textView.text
    3 if enteredText?.count ?? 0 >= 3 {
    4 let searchQuery: TTSearchQuery = TTSearchQueryBuilder.create(withTerm: enteredText ?? "").withTypeAhead(true).withLang("en-US").build()
    5 self.tomtomSearchAPI.search(with: searchQuery)
    6 }
    7}
  4. The TTSearchQueryBuilder is used to create searchQuery object. Because a searched term can be partial, the withTypeAhead(true) parameter is used to get better results from the search engine. Search engine results are handled by one of the two methods:

  5. func search(_ search: TTSearch, completedWith response: TTSearchResponse)

  6. func search(_ search: TTSearch, failedWithError error: TTResponseError) See the following code example:

    1func search(_ search: TTSearch, completedWith response: TTSearchResponse) {
    2 searchResults.removeAll()
    3 for result in response.results {
    4 let resultTuple = (result.address.freeformAddress!, result.position)
    5 searchResults.append(resultTuple)
    6 }
    7 if CLLocationCoordinate2DIsValid(currentLocationCoords) {
    8 searchResults.append(("-- Your Location --", currentLocationCoords))
    9 searchResults.sort() { sh.0 < .0 }
    10 }
    11 autocompleteTableView?.reloadData()
    12 autocompleteTableView.isHidden = false
    13}
    14
    15func search(_ search: TTSearch, failedWithError error: TTResponseError) {
    16 print(error.description)
    17}
  7. When the search query has completed with a successful response a tuple with an address and position of that address is created.

Search result addresses are used to create an autocomplete table view so that the user can pick the correct address from the list which then becomes the departure or the destination position.

When both the departure and the destination positions are set, a user can pick the arrival time, preparation time and travel mode. These parameters are gathered and passed to the CountDownViewController using “prepare for segue” method so that application can switch the count down screen. See the following code example.

1override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
2 if segue.identifier == self.timeShowSegueIdentifier {
3 let destViewController: CountDownViewController = segue.destination as! CountDownViewController
4 destViewController.travelMode = travelMode
5 destViewController.arriveAtTime = getArrivalDate()
6 destViewController.preparationTime = preparationTime
7 destViewController.departureLocation = departureCoords
8 destViewController.destinationLocation = destinationCoords
9 }
10}

TomTom Map display module:

A CountDownViewController uses map and routing modules from the TomTom Maps SDK.

  1. To start working with TomTom Map, a user has to import the TomTomOnlineSDKMaps package on top of the file.
  2. Then, the user:
  3. Creates the TTMapView object which will be initialized later in the code.
  4. Creates the TTMapStyleDefaultConfiguration and TTMapConfiguration objects in order to pass them into the TTMapView constructor methods.

The mapView object is then initialized inside the initTomTomServices and initTomTomMap methods, where proper constraints together with an insets has been set and onMapReadyCompletion block has been defined.

Inside that block the mapView object is fully initialized and the user can start interacting with it.

In the presented application, user is requesting a route update so that a route can be visualized on the map. See the following code example.

1import UIKit
2import TomTomOnlineSDKMaps
3import TomTomOnlineSDKRouting
4class CountDownViewController: UIViewController, TTRouteResponseDelegate{
5 var mapView: TTMapView!
6 override func viewDidLoad() {
7 super.viewDidLoad()
8 initTomTomServices()
9 }
10
11 fileprivate func initTomTomMap() {
12 let style = TTMapStyleDefaultConfiguration()
13 let config = TTMapConfigurationBuilder.create()
14 .withMapKey(Key.Map)
15 .withTrafficKey(Key.Traffic)
16 .withMapStyleConfiguration(style)
17 .build()
18 self.mapView = TTMapView(frame: self.view.frame, mapConfiguration: config)
19 self.view.insertSubview(mapView, belowSubview: countDownView)
20 self.mapView.translatesAutoresizingMaskIntoConstraints = false
21 self.mapView.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
22 self.mapView.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true
23 self.mapView.topAnchor.constraint(equalTo: self.countDownView.bottomAnchor).isActive = true
24 self.mapView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor).isActive = true
25 }
26
27 fileprivate func initTomTomServices() {
28 initTomTomMap()
29 self.ttRoute.delegate = self
30 let insets = UIEdgeInsets.init(top: 30 * UIScreen.main.scale, left: 10 * UIScreen.main.scale, bottom: 30 * UIScreen.main.scale, right:10 * UIScreen.main.scale)
31 self.mapView.contentInset = insets
32 self.mapView.onMapReadyCompletion {
33 self.mapView.isShowsUserLocation = true
34 self.present(self.progressDialog, animated: true, completion: nil)
35 self.requestRouteUpdate()
36 }
37 }

TomTom Routing module:

The CountDown view controller uses the TomTom Routing APIs to perform routing requests and operations like drawing a route on the map or displaying start and end route icons.

  1. First, a TomTomOnlineSDKRouting is imported on top of the file, and a ttRoute variable is declared and initialized. See the following code example.

    1import TomTomOnlineSDKMaps
    2import TomTomOnlineSDKRouting
    3
    4class CountDownViewController: UIViewController{
    5var mapView: TTMapView!
    6
    7let ttRoute = TTRoute(key: Key.Routing)
  2. Then, the CountDownViewController conforms to the TTRouteResponseDelegate class and can be set as a delegate inside the ttRoute object. See the following code example.

    1class CountDownViewController: UIViewController, TTRouteResponseDelegate {
    2var mapView: TTMapView!
    3
    4fileprivate func initTomTomServices () {
    5 self.ttRoute.delegate = self
  3. After assigning a delegate to the CountDownViewController, the “request route update” method can be called. See the following code example.

    1fileprivate func requestRouteUpdate() {
    2 let routeQuery = TTRouteQueryBuilder.create(withDest: self.destinationLocation, andOrig: self.departureLocation)
    3 .withArriveAt(self.arriveAtTime)
    4 .withTraffic(true)
    5 .withTravelMode(self.travelMode)
    6 .withRouteType(.fastest)
    7 .build()
    8 self.ttRoute.plan(with: routeQuery)
    9}
  4. The requestRouteUpdate creates the routeQuery object and passes it to the ttRoute object inside a plan(with: query ) method as an argument. TTRouteQueryBuilder is used to simplify building a routeQuery object with a few helper methods:

  • withArriveAt – a date and time of arrival at the destination point. It’s an important method inside the application because it changes a departure date which is later used to calculate time left before the departure.
  • withTraffic – when enabled, current traffic information is used during the route planning, when disabled – only historical traffic data is used.
  • withTravelMode – in the application user can choose one of the following: car, taxi or pedestrian. Other possible parameters are listed here:
  • withRouteType – allow the users to select one type of route from: fastest, shortest, thrilling or eco.
  1. When the ttRoute.plan(with: routeQuery) method will be executed, the ttRoute object will call the TTRouteResponseDelegate methods which are implemented inside CountDownViewController.

    1func route(_ route: TTRoute, completedWith result: TTRouteResult) {
    2 func startTimer() {
    3 self.countDownTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
    4 }
    5
    6 func drawRouteOnTomTomMap(_ route: TTFullRoute) {
    7 let mapRoute = TTMapRoute(coordinatesData: route, with: TTMapRouteStyle.defaultActive(), imageStart: TTMapRoute.defaultImageDeparture(), imageEnd: TTMapRoute.defaultImageDestination())
    8 mapView.routeManager.add(mapRoute)
    9 mapView.routeManager.showRouteOverview(mapRoute)
    10 }
    11
    12 func presentTrafficUpdateDialog(message: String) {
    13 let trafficChangedDialog = initStoryBoardDialog(identifier: "trafficUpdateMessage")
    14 (trafficChangedDialog as! TrafficUpdateViewController).message = message
    15 presentTrafficDialog(viewController: trafficChangedDialog)
    16 }
    17
    18 func presentTrafficDialog(viewController: UIViewController, for seconds: Double = 3.0) {
    19 self.present(viewController, animated: true)
    20 DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + seconds) {
    21 viewController.dismiss(animated: true)
    22 }
    23 }
    24
    25 guard let plannedRoute = result.routes.first else {
    26 return
    27 }
    28
    29 self.progressDialog.dismiss(animated: true, completion: nil)
    30 let newDepartureTime = plannedRoute.summary.departureTime
    31 self.travelTimeInSeconds = plannedRoute.summary.travelTimeInSecondsValue
    32 if (self.previousDepartureTime == nil) {
    33 self.previousDepartureTime = newDepartureTime
    34 drawRouteOnTomTomMap(plannedRoute)
    35 startTimer()
    36 updateTimer()
    37 }
    38 else if self.previousDepartureTime != newDepartureTime {
    39 presentTrafficUpdateDialog(message: "Route recalculated due to change in traffic: \(Int(self.previousDepartureTime.timeIntervalSince(newDepartureTime!)))sec")
    40 self.previousDepartureTime = newDepartureTime
    41 }
    42 else if self.previousDepartureTime == newDepartureTime {
    43 let noTrafficDialog = initStoryBoardDialog(identifier: "trafficNoUpdateMessage")
    44 presentTrafficDialog(viewController: noTrafficDialog)
    45 }
    46}
    47
    48func route(_ route: TTRoute, completedWith responseError: TTResponseError) {
    49 func displayErrorDialog() {
    50 let alertDialog = UIAlertController(title: "Error", message: "No routes found satisfying requested time. Please choose different arrival time and try again." , preferredStyle: .alert)
    51 let dialogAction = UIAlertAction(title: "Dismiss", style: .default, handler: { _ in
    52 self.dismiss(animated: true, completion: nil)
    53 })
    54 alertDialog.addAction(dialogAction)
    55 self.present(alertDialog, animated: true, completion: nil)
    56 }
    57
    58 self.progressDialog.dismiss(animated: true, completion: nil)
    59 if self.previousDepartureTime == nil {
    60 displayErrorDialog()
    61 }
    62}
  2. When planning of the route will not succeed, the route(completedWith error: TTResponseError) method is executed.

  3. Inside that method the application creates and display an error dialog to the user.

  4. When user taps on the “Dismiss” action the CountDownViewController will be dismissed and the application will display its first screen, so that the user can re-plan the route again.

  5. When the planning of the route succeeds, the route(completedWith result: TTRouteResult) method will be called. After receiving routing results, the new departure time is saved and few checks are performed:

  6. When a self.previousDepartureTime variable is nil, it means that it’s a first routing call since the CountDownViewController did load. In this case:

  7. The drawRouteOnTomTomMap function is called and the route is displayed on the map. See the following code example.

    1func drawRouteOnTomTomMap(_ route: TTFullRoute) {
    2 let mapRoute = TTMapRoute(coordinatesData: route, with: TTMapRouteStyle.defaultActive(), imageStart: TTMapRoute.defaultImageDeparture(), imageEnd: TTMapRoute.defaultImageDestination())
    3 mapView.routeManager.add(mapRoute)
    4 mapView.routeManager.showRouteOverview(mapRoute)
    5}
  8. The startTimer method initializes and starts a timer with one second interval. Timer will be used to count down the time which is left to the departure time. updateTimer method will be called at each tick of the timer.

    1func startTimer() {
    2 self.countDownTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
    3}
  9. Lastly, the updateTimer method is called to update all count down labels immediately without waiting for the one second interval before the first timer tick. Method updateTimer will be described later.

  10. When the self.previousDepartureTime variable is different than the newDepartureTime it means, that there was an update in traffic conditions. The application saves the newDepartureTime inside the self.previousDepartureTime variable and displays a message to the user:

  11. When the self.previousDepartureTime is the same as the newDepartureTime variable it means that traffic conditions are the same so there is no action besides displaying the proper dialog to the user:

When the CountDown ViewController is displayed for the first time and route planning has been finished successfully, the timer is initialized and starts calling the updateTimer method each and every second. See the following code example.

1@objc func updateTimer() {
2 func prepareFinalAlert() -> UIAlertController {
3 let timeToLeaveAlert = UIAlertController(title: "Time's UP!", message: "Time to leave!", preferredStyle: .alert)
4 let timeToLeaveWhateverAction = UIAlertAction(title: "Whatever", style: .default, handler: { _ in
5 let overTimeDialog = self.initStoryBoardDialog(identifier: "overtimeDialog")
6 (overTimeDialog as! OverTimeViewController).parentDelegate = self
7 self.present(overTimeDialog, animated: true, completion: nil)
8 })
9 let onMyWayAction = UIAlertAction(title: "On My Way!", style: .default, handler: { _ in
10 self.performSegue(withIdentifier: "safeTravelsSegue", sender: self)
11 })
12 timeToLeaveAlert.addAction(timeToLeaveWhateverAction)
13 timeToLeaveAlert.addAction(onMyWayAction)
14 return timeToLeaveAlert
15 }
16
17 let routeRecalculationDelayInSeconds = 60
18 countDownSeconds += 1
19 let currentTime = Date()
20 if currentTime < previousDepartureTime {
21 let timeInterval = DateInterval(start: currentTime, end: previousDepartureTime)
22 displayTimeToLeave(timeInterval)
23
24 if countDownSeconds % routeRecalculationDelayInSeconds == 0
25 && timeInterval.duration > 5 {
26 self.requestRouteUpdate()
27 }
28 }
29 else {
30 resetTimer()
31 let finalTimeToLeaveAlert = prepareFinalAlert()
32 self.present(finalTimeToLeaveAlert, animated: true, completion: nil)
33 }
34}

First, a countDownSeconds variable is incremented and a current time is stored in the currentTime variable. Now, there are two cases which will be handled inside the updateTimer function.

  1. When the currentTime variable occurs earlier than the self.previousDepartureTime variable, it means that the user still has some time to leave a departure point. To visualize how much time is left to leave, time interval between currentTime and previousDepartureTime is calculated and displayed to the user by the displayTimeToLeave function. Every 60 seconds timer invokes requestRouteUpdate function which updates departure times based on the current traffic conditions.
  2. The second case occurs when the current time is the same or later than the previousDepartureTime. In that case it means that the timer finished its count down and the user should leave the departure position in order to be on the requested arrival time at the destination point. The countDownSeconds variable is set to zero, and the final dialog is displayed to the user.

Happy Coding