Search along a route

Overview

This tutorial shows how to use the TomTom Maps SDK for iOS to create an application that helps a user find points of interest along a planned route. The application is written in Objective-C and Swift language. If you are interested in another example, please check the Time to leave tutorial.

It shows how to use:

  • The TomTom Map SDK module to display a map, including markers with custom icons and balloons.
  • The TomTom Routing SDK module to plan routes with and without waypoints.
  • The TomTom Search SDK module to search for points of interest (POIs) and to geocode map positions.

An end user can start interacting with the application by planning a route with departure and destination points:

  • One long press on the map sets a departure point.
  • A second long press sets a destination point and draws a route between those two points on the map.

When the route is visible on the map, the user can type a POI name or category into a search field or click on any of the predefined POI category buttons (gas station, restaurant, ATM). The map displays markers for POIs that match the user's request. The user can add one of the displayed POIs to their planned route by tapping the marker on the map and choosing the "Add to my route" button inside the marker balloon that is then displayed. The route is recalculated and redrawn to include the selected point.

Prerequisites

  1. Make sure that the CocoaPods dependency manager is installed on your computer: https://cocoapods.org.
  2. Create a new App project named SearchAlongARoute.. Choose either Objective-C or Swift and make sure that Storyboard is selected.
  3. Go to the console and create a pod file by executing a pod init command.
  4. Copy and paste SDK module dependencies into the newly created Podfile:
    1pod 'TomTomOnlineSDKMaps', ‘2.4.713’
    2pod 'TomTomOnlineSDKSearch', ‘2.4.713’
    3pod 'TomTomOnlineSDKRouting', ‘2.4.713’
    4pod 'TomTomOnlineSDKMapsUIExtensions', ‘2.4.713’
    Install the SDK modules by typing pod install from the console. After this is finished close your project and open *.xcworkspace file with XCode. For more details about this step, please see the downloads section.
  5. If you don't have an API key visit a How to get a TomTom API key site and create one. The key will be used later in the code.

Map initialization

To initialize a TomTom map, import the TomTomOnlineSDKMaps module into your project file and add a placeholder for the new API key. Paste your key in the YOUR_API_KEY placeholder.

OBJECTIVEC
SWIFT
1#import "ViewController.h"
2#import <TomTomOnlineSDKMaps/TomTomOnlineSDKMaps.h>
3
4NSString *const API_KEY = @"YOUR_API_KEY";
1public class Key: NSObject {
2 @objc public static let Map = "YOUR_API_KEY"
3 @objc public static let Routing = "YOUR_API_KEY"
4 @objc public static let Search = "YOUR_API_KEY"
5 @objc public static let Traffic = "YOUR_API_KEY"
6}

Now, add a TTMapView property into a ViewController interface.

OBJECTIVEC
SWIFT
1@interface ViewController ()
2@property(nonatomic) IBOutlet TTMapView *tomtomMap;
3@end
1class ViewController {
2 @IBOutlet private var mapView: TTMapView!
3}

Next, add a following lines of code into the viewDidLoad method.

OBJECTIVEC
SWIFT
1- (void)viewDidLoad {
2 [super viewDidLoad];
3
4 TTMapStyleDefaultConfiguration *style = [[TTMapStyleDefaultConfiguration alloc] init];
5 TTMapConfiguration *config = [[[[[TTMapConfigurationBuilder alloc]
6 withMapKey:API_KEY]
7 withTrafficKey:API_KEY]
8 withMapStyleConfiguration:style] build];
9 self.tomtomMap = [[TTMapView alloc] initWithFrame:self.view.frame mapConfiguration:config];
10 [self.view addSubview:self.tomtomMap];
11}
1let style = TTMapStyleDefaultConfiguration()
2let config = TTMapConfigurationBuilder.create()
3 .withMapKey(Key.Map)
4 .withTrafficKey(Key.Traffic)
5 .withMapStyleConfiguration(style)
6 .build()
7self.tomtomMap = TTMapView(frame: self.view.frame, mapConfiguration: config)
8self.view.addSubview(self.tomtomMap)

Run your application. You should see a map.

The TomTom map handles zooming, panning, rotating and double tapping gestures. In this application you need to add additional map interactions including location handling, long map press events, drawing routes and markers.

Now add import statements inside the ViewController file:

OBJECTIVEC
SWIFT
1#import <TomTomOnlineSDKSearch/TomTomOnlineSDKSearch.h>
2#import <TomTomOnlineSDKRouting/TomTomOnlineSDKRouting.h>
3#import <TomTomOnlineSDKMapsUIExtensions/TomTomOnlineSDKMapsUIExtensions.h>
1import TomTomOnlineSDKSearch
2import TomTomOnlineSDKRouting
3import TomTomOnlineSDKMapsUIExtensions

Your ViewController class must conform to the protocols:

  • TTMapViewDelegate
  • TTAnnotationDelegate
  • TTAlongRouteSearchDelegate

Add an initTomTomServices method to the ViewController class where the Map Display API modules are initialized. At the same time, add an initUIViews method where User Interface (UI) elements are initialized. Move the map initialization code in the initTomTomServices method. Once the map view is ready to be used the onMapReady method is called, in which you need to enable showing user current location and enable clustering annotations.

OBJECTIVEC
SWIFT
1#import "ViewController.h"
2
3#import <TomTomOnlineSDKMaps/TomTomOnlineSDKMaps.h>
4#import <TomTomOnlineSDKSearch/TomTomOnlineSDKSearch.h>
5#import <TomTomOnlineSDKRouting/TomTomOnlineSDKRouting.h>
6#import <TomTomOnlineSDKMapsUIExtensions/TomTomOnlineSDKMapsUIExtensions.h>
7
8NSString *const API_KEY = @"YOUR_API_KEY";
9
10@interface ViewController() <TTMapViewDelegate, TTAnnotationDelegate, TTAlongRouteSearchDelegate>
11@property (strong, nonatomic) TTMapView *tomtomMap;
12@end
13
14@implementation ViewController
15- (void)viewDidLoad {
16 [super viewDidLoad];
17 [self initTomTomServices];
18 [self initUIViews];
19}
20
21- (void)initTomTomServices {
22 TTMapStyleDefaultConfiguration *style = [[TTMapStyleDefaultConfiguration alloc] init];
23 TTMapConfiguration *config = [[[[[TTMapConfigurationBuilder alloc]
24 withMapKey:API_KEY]
25 withTrafficKey:API_KEY]
26 withMapStyleConfiguration:style] build];
27 self.tomtomMap = [[TTMapView alloc] initWithFrame:self.view.frame mapConfiguration:config];
28 [self.view addSubview:self.tomtomMap];
29}
30
31- (void)initUIViews {
32}
33@end
34
35- (void)onMapReady:(TTMapView *)mapView {
36 self.tomtomMap.showsUserLocation = YES;
37 self.tomtomMap.annotationManager.clustering = true;
38}
1import UIKit
2import TomTomOnlineSDKMaps
3import TomTomOnlineSDKSearch
4import TomTomOnlineSDKRouting
5import TomTomOnlineSDKMapsUIExtensions
6
7
8class ViewController: UIViewController, TTMapViewDelegate, TTAlongRouteSearchDelegate, TTAnnotationDelegate {
9
10 @IBOutlet private var tomtomMap: TTMapView!
11
12 override func viewDidLoad() {
13 super.viewDidLoad()
14 self.initTomtomServices()
15 self.initUIViews()
16 }
17
18 private func initTomtomServices(){
19 let style = TTMapStyleDefaultConfiguration()
20 let config = TTMapConfigurationBuilder.create()
21 .withMapKey(Key.Map)
22 .withTrafficKey(Key.Traffic)
23 .withMapStyleConfiguration(style)
24 .build()
25 self.tomtomMap = TTMapView(frame: self.view.frame, mapConfiguration: config)
26 self.view.addSubview(self.tomtomMap)
27 }
28
29 private func initUIViews(){
30 }
31
32 func onMapReady(_ mapView: TTMapView) {
33 self.tomtomMap.isShowsUserLocation = true
34 self.tomtomMap.annotationManager.clustering = true
35 }
36}

Drawing a route on the map

Drawing a route on the map requires that the following additional properties be defined in the ViewController interface:

  • TTRoute
  • TTReverseGeocoder
  • TTAlongRouteSearch – used in later part of this tutorial
  • Departure coordinates
  • Destination coordinates
  • Waypoint coordinates
  • TTFullRoute
OBJECTIVEC
SWIFT
1@property(strong,nonatomic) TTRoute*route;
2@property(strong,nonatomic) TTReverseGeocoder *reverseGeocoder;
3@property(strong,nonatomic) TTAlongRouteSearch *alongRouteSearch;
4@property TTFullRoute*fullRoute;
5@property CLLocationCoordinate2D departurePosition;
6@property CLLocationCoordinate2D destinationPosition;
7@property CLLocationCoordinate2D wayPointPosition;
8@property TTAnnotationImage* departureImage;
1private var route: TTRoute!
2private var reverseGeocoder: TTReverseGeocoder!
3private var alongRouteSearch: TTAlongRouteSearch!
4private var fullRoute: TTFullRoute!
5private var departurePosition: CLLocationCoordinate2D? = nil
6private var destinationPosition: CLLocationCoordinate2D? = nil
7private var wayPointPosition: CLLocationCoordinate2D? = nil
8private var departureImage: TTAnnotationImage!

Initialize required fields and services inside the initTomTomServices method. Your ViewController class is used here as a delegate receiving an event notification from the TomTom Map, Routing and Search services.

OBJECTIVEC
SWIFT
1- (void)initTomTomServices {
2 TTMapStyleDefaultConfiguration *style = [[TTMapStyleDefaultConfiguration alloc] init];
3 TTMapConfiguration *config = [[[[[TTMapConfigurationBuilder alloc]
4 withMapKey:API_KEY]
5 withTrafficKey:API_KEY]
6 withMapStyleConfiguration:style] build];
7 self.tomtomMap = [[TTMapView alloc] initWithFrame:self.view.frame mapConfiguration:config];
8 [self.view addSubview:self.tomtomMap];
9
10 self.tomtomMap.delegate = self;
11 self.tomtomMap.annotationManager.delegate = self;
12
13 self.reverseGeocoder = [[TTReverseGeocoder alloc] initWithKey:API_KEY];
14 self.route = [[TTRoute alloc] initWithKey:API_KEY];
15 self.alongRouteSearch = [[TTAlongRouteSearch alloc] initWithKey:API_KEY];
16 self.alongRouteSearch.delegate = self;
17 self.departurePosition = kCLLocationCoordinate2DInvalid;
18 self.destinationPosition = kCLLocationCoordinate2DInvalid;
19 self.wayPointPosition = kCLLocationCoordinate2DInvalid;
20}
1private func initTomtomServices(){
2 let style = TTMapStyleDefaultConfiguration()
3 let config = TTMapConfigurationBuilder.create()
4 .withMapKey(Key.Map)
5 .withTrafficKey(Key.Traffic)
6 .withMapStyleConfiguration(style)
7 .build()
8
9 self.tomtomMap = TTMapView(frame: self.view.frame, mapConfiguration: config)
10 self.view.addSubview(self.tomtomMap)
11
12 self.tomtomMap.delegate = self
13 self.tomtomMap.annotationManager.delegate = self
14
15 self.reverseGeocoder = TTReverseGeocoder(key: Key.Search)
16 self.route = TTRoute(key: Key.Routing)
17 self.alongRouteSearch = TTAlongRouteSearch(key: Key.Search)
18 self.alongRouteSearch.delegate = self
19}

Initialize an icon for a departure map annotation inside the initUIViews method.You also need to add an icon named ic_map_route_departure to the Assets.xcassets directory.

OBJECTIVEC
SWIFT
self.departureImage = [TTAnnotationImage createPNGWithName:@"ic_map_route_departure"];
self.departureImage = TTAnnotationImage.createPNG(with: UIImage(named: "ic_map_route_departure")!)!

You need a clearMap function, where all the markers and the route are removed from the map.

OBJECTIVEC
SWIFT
1- (void)clearMap {
2 [self.tomtomMap.routeManager removeAllRoutes];
3 [self.tomtomMap.annotationManager removeAllAnnotations];
4 self.departurePosition = kCLLocationCoordinate2DInvalid;
5 self.destinationPosition = kCLLocationCoordinate2DInvalid;
6 self.wayPointPosition = kCLLocationCoordinate2DInvalid;
7 self.fullRoute = nil;
8}
1private func clearMap(){
2 self.tomtomMap.routeManager.removeAllRoutes()
3 self.tomtomMap.annotationManager.removeAllAnnotations()
4 self.departurePosition = nil
5 self.destinationPosition = nil
6 self.wayPointPosition = nil
7 self.fullRoute = nil
8}

Now add an implementation to the method didLongPress that handles long press events on the map.

OBJECTIVEC
SWIFT
1- (BOOL)isDestinationPositionSet {
2 return CLLocationCoordinate2DIsValid(self.destinationPosition);
3}
4
5- (BOOL)isDeparturePositionSet {
6 return CLLocationCoordinate2DIsValid(self.departurePosition);
7}
8
9- (void)mapView:(TTMapView *)mapView didLongPress:(CLLocationCoordinate2D)coordinate {
10 if ([self isDeparturePositionSet] && [self isDestinationPositionSet]) {
11 [self clearMap];
12 } else {
13 [self handleLongPress:coordinate];
14 }
15}
16
17- (void)handleLongPress:(CLLocationCoordinate2D)coordinate {
18 TTReverseGeocoderQuery *query = [[TTReverseGeocoderQueryBuilder createWithCLLocationCoordinate2D:coordinate] build];
19
20 [self.reverseGeocoder reverseGeocoderWithQuery:query completionHandle:^(TTReverseGeocoderResponse *response, TTResponseError *error) {
21 if (response.result.addresses.count > 0) {
22 TTReverseGeocoderFullAddress *firstAddress = response.result.addresses.firstObject;
23 NSString *address = firstAddress.address.freeformAddress ? firstAddress.address.freeformAddress : @" ";
24 [self processGeocoderResponse:firstAddress.position address:address];
25 }
26 }];
27}
28
29- (void)processGeocoderResponse:(CLLocationCoordinate2D)geocodedPosition address:(NSString *)address {
30 if (!CLLocationCoordinate2DIsValid(self.departurePosition)) {
31 self.departurePosition = geocodedPosition;
32 [self createAndDisplayMarkerAtPosition:self.departurePosition withAnnotationImage:self.departureImage andBalloonText:address];
33 } else {
34 self.destinationPosition = geocodedPosition;
35 [self drawRouteWithDeparture:self.departurePosition andDestination:self.destinationPosition];
36 }
37}
1private var isDeparturePositionSet: Bool {
2 return departurePosition != nil
3}
4
5private var isDestinationPositionSet: Bool {
6 return destinationPosition != nil
7}
8
9func mapView(_ mapView: TTMapView, didLongPress coordinate: CLLocationCoordinate2D) {
10 self.dismissKeyboard()
11 if self.isDeparturePositionSet && self.isDestinationPositionSet {
12 self.clearMap()
13 }else {
14 self.handleLongPress(coordinate)
15 }
16}
17
18private func handleLongPress(_ coordinate: CLLocationCoordinate2D){
19 let query = TTReverseGeocoderQueryBuilder.create(with: coordinate).build()
20 self.reverseGeocoder.reverseGeocoder(with: query, completionHandle: {(response, error) -> Void in
21 if let error = error {
22 self.handleApiError(error)
23 } else if let firstAddress = response?.result.addresses.first {
24 let address = firstAddress.address.freeformAddress != nil ? firstAddress.address.freeformAddress : " "
25 self.processGeocoderResponse(coordinate: firstAddress.position, address: address!)
26 }
27 })
28}
29
30private func processGeocoderResponse(coordinate: CLLocationCoordinate2D, address: String){
31 if !self.isDeparturePositionSet {
32 self.departurePosition = coordinate
33 self.createAndDisplayMarkerAtPosition(coordinate, withAnnotationImage: self.departureImage, andBaloonText: address)
34 } else if !isDestinationPositionSet {
35 self.destinationPosition = coordinate
36 self.drawRouteWithDeparture()
37 }
38}

The didLongPress function calls a reverseGeocoder's reverseGeocoderWithQuery method. This method checks if a road can be found in the place where a long press is registered on the map.

If the reverse geocoding call is successful and contains valid results, the effect of the long press depends on context:

  • The first long press on the map sets the departurePosition object.
  • A second long press sets the destinationPosition object and draws a route on the map.
  • A third long press removes any destination and departure markers and the route from the map.

Next add functions to send a route request to the Routing API and to draw a route from the response on the map.

OBJECTIVEC
SWIFT
1- (void)drawRouteWithDeparture:(CLLocationCoordinate2D)departure andDestination:(CLLocationCoordinate2D)destination {
2 [self drawRouteWithDeparture:departure andDestination:destination andWayPoint:kCLLocationCoordinate2DInvalid];
3}
4
5- (void)drawRouteWithDeparture:(CLLocationCoordinate2D)departure andDestination:(CLLocationCoordinate2D)destination andWayPoint:(CLLocationCoordinate2D)wayPoint {
6 TTRouteQuery *query = [self createRouteQueryWithOrigin:departure andDestination:destination andWayPoint:wayPoint];
7 [self.route planRouteWithQuery:query completionHandler:^(TTRouteResult *result, TTResponseError *error) {
8 if (result.routes.count > 0) {
9 [self addActiveRouteToMap:result.routes.firstObject];
10 }
11 }];
12}
13
14- (TTRouteQuery *)createRouteQueryWithOrigin:(CLLocationCoordinate2D)origin andDestination:(CLLocationCoordinate2D)destination andWayPoint:(CLLocationCoordinate2D)wayPoint {
15 TTRouteQueryBuilder *builder = [TTRouteQueryBuilder createWithDest:destination andOrig:origin];
16 if (CLLocationCoordinate2DIsValid(wayPoint)) {
17 [builder withWayPoints:@[[NSValue value:&wayPoint withObjCType:@encode(CLLocationCoordinate2D)]]];
18 }
19 return [builder build];
20}
21
22- (void)addActiveRouteToMap:(TTFullRoute *)route {
23 [self.tomtomMap.routeManager removeAllRoutes];
24 self.fullRoute = route;
25 if (!CLLocationCoordinate2DIsValid(self.wayPointPosition)) {
26 [self.tomtomMap.annotationManager removeAllAnnotations];
27 }
28 TTMapRoute *mapRoute = [TTMapRoute routeWithCoordinatesData:self.fullRoute withRouteStyle:TTMapRouteStyle.defaultActiveStyle
29 imageStart:TTMapRoute.defaultImageDeparture imageEnd:TTMapRoute.defaultImageDestination];
30 [self.tomtomMap.routeManager addRoute:mapRoute];
31}
1private func drawRouteWithDeparture(wayPoint: CLLocationCoordinate2D? = nil){
2 guard let departurePosition = self.departurePosition, let destinationPosition = self.destinationPosition else {
3 self.displayMessage("Departure position or destination position were not set.")
4 return
5 }
6 let routeQuery = createRouteQuery(departure: departurePosition, destination: destinationPosition, wayPoint: self.wayPointPosition)
7 self.route.plan(with: routeQuery, completionHandler: { (routeResult: TTRouteResult?, error: TTResponseError?) -> Void in
8 if let error = error {
9 self.handleApiError(error)
10 self.clearMap()
11 } else if let result = routeResult {
12 self.fullRoute = result.routes.first
13 if let fullRoute = self.fullRoute {
14 self.addActiveRouteToMapView(fullRoute)
15 }
16 }
17 })
18}
19
20private func createRouteQuery(departure: CLLocationCoordinate2D, destination: CLLocationCoordinate2D, wayPoint: CLLocationCoordinate2D?) -> TTRouteQuery {
21 let builder = TTRouteQueryBuilder.create(withDest: destination, andOrig: departure)
22 if var wayPoint = wayPoint {
23 builder.withWayPoints(&wayPoint, count: 1)
24 }
25 return builder.build()
26}
27
28private func addActiveRouteToMapView(_ fullRoute: TTFullRoute?) {
29 let iconStart = UIImage(named: "ic_map_route_departure")
30 let iconEnd = UIImage(named: "ic_map_route_destination")
31 if let fullRoute = fullRoute {
32 self.tomtomMap.routeManager.removeAllRoutes()
33 if self.wayPointPosition == nil {
34 self.tomtomMap.annotationManager.removeAllAnnotations()
35 }
36 let mapRoute = TTMapRoute(coordinatesData: fullRoute, with: TTMapRouteStyle.defaultActive(), imageStart: iconStart, imageEnd: iconEnd)
37 self.tomtomMap.routeManager.add(mapRoute)
38 self.tomtomMap.routeManager.showAllRoutesOverview()
39 }
40}

The createRouteQuery method returns a routeQuery object:

  • With additional waypoints (if the wayPoint field is a valid 2D coordinate).
  • Without additional waypoints (if the wayPoint field is not a valid 2D coordinate).

The drawRouteWithDeparture method calls the Routing API. If the response is successful, an active route is drawn on the map.

Add a createAndDisplayMarkerAtPosition method to display a departure position marker if the destination position is not set:

OBJECTIVEC
SWIFT
1- (NSString *)coordinatesToString:(CLLocationCoordinate2D)coords {
2 return [NSString stringWithFormat:@"%@,%@", [@(coords.latitude) stringValue], [@(coords.longitude) stringValue]];
3}
4
5- (void)createAndDisplayMarkerAtPosition:(CLLocationCoordinate2D)coords withAnnotationImage:(TTAnnotationImage *)image andBalloonText:(NSString *)text {
6 self.positionsPoisInfo[[self coordinatesToString:coords]] = text;
7 [self.tomtomMap.annotationManager addAnnotation:[TTAnnotation annotationWithCoordinate:coords annotationImage:image anchor:TTAnnotationAnchorCenter type:TTAnnotationTypeFocal]];
8}
1private func coordinatesToString(_ coordinate: CLLocationCoordinate2D) -> String {
2 return "\(coordinate.latitude),\(coordinate.longitude)"
3}
4
5private func createAndDisplayMarkerAtPosition(_ coordinate: CLLocationCoordinate2D, withAnnotationImage image: TTAnnotationImage, andBaloonText: String){
6 self.positionsPoisInfo[self.coordinatesToString(coordinate)] = andBaloonText
7 let annotation = TTAnnotation(coordinate: coordinate, annotationImage: image, anchor: TTAnnotationAnchor.center, type: TTAnnotationType.focal)
8 self.tomtomMap.annotationManager.add(annotation)
9}

Now add dictionary property where you can store coordinates with a balloon text to display when any map marker is tapped.

OBJECTIVEC
SWIFT
@property NSMutableDictionary *positionsPoisInfo;
private var positionsPoisInfo: [String : String]!

Make sure that you initialize the dictionary inside the initUIViews method:

OBJECTIVEC
SWIFT
self.positionsPoisInfo = [[NSMutableDictionary alloc] init];
self.positionsPoisInfo = [:]

Now you can draw the route on the map by using long presses in chosen locations.

Searching for POIs along the route

Modify the Main.storyboard file by adding a SearchBar control. Add constraints to the SearchBar control to make sure that it will be displayed correctly on all of the devices.

Now add constraints to the TTMapView property:

OBJECTIVEC
SWIFT
1- (void)initTomTomMapConstraints {
2 [self.tomtomMap setTranslatesAutoresizingMaskIntoConstraints:false];
3 [[self.tomtomMap.leftAnchor constraintEqualToAnchor:self.view.leftAnchor] setActive:true];
4 [[self.tomtomMap.topAnchor constraintEqualToAnchor:self.view.topAnchor] setActive:true];
5 [[self.tomtomMap.rightAnchor constraintEqualToAnchor:self.view.rightAnchor] setActive:true];
6 [[self.tomtomMap.bottomAnchor constraintEqualToAnchor:self.searchBar.topAnchor] setActive:true];
7}
1private func initTomtomMapConstraints(){
2 self.tomtomMap.translatesAutoresizingMaskIntoConstraints = false
3 let left = self.tomtomMap.leftAnchor.constraint(equalTo: self.view.leftAnchor)
4 let right = self.tomtomMap.rightAnchor.constraint(equalTo: self.view.rightAnchor)
5 let top = self.tomtomMap.topAnchor.constraint(equalTo: self.view.topAnchor)
6 let bottom = self.tomtomMap.bottomAnchor.constraint(equalTo: self.searchBar.topAnchor)
7 NSLayoutConstraint.activate([left, right, top, bottom])
8}

Execute the initTomTomMapConstraints function inside initTomTomServices.

In the next step you need to make sure that the SearchBar which you just added is always above a keyboard when it is visible.

First add two constant fields above the interface declaration in the ViewController file:

OBJECTIVEC
SWIFT
static const int KEYBOARD_SHOW_MULTIPLIER = 1;
static const int KEYBOARD_HIDE_MULTIPLIER = -1;
private static let KEYBOARD_SHOW_MULTIPLIER = 1;
private static let KEYBOARD_HIDE_MULTIPLIER = -1;

and two properties inside the ViewController class:

OBJECTIVEC
SWIFT
@property(weak, nonatomic) IBOutlet NSLayoutConstraint *bottomConstraint;
@property Boolean keyboardShown;
private var isKeyboardShown: Bool!
@IBOutlet private var bottomConstraint: NSLayoutConstraint!

A bottomConstraint property is adjusted with the size of the keyboard shown on the phone screen when the user starts typing inside the SearchBar. A keyboardShown property is used to store information if keyboard is currently visible.

Make sure that the bottomConstraint property field is connected with a SearchBar bottom constraint from the Main.storyboard file.

Next add an initKeyboardNotificationEvents method where keyboard events are assigned to methods executed whenever a specific event is triggered.

OBJECTIVEC
SWIFT
1- (void)initKeyboardNotificationEvents {
2 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
3 [nc addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
4 [nc addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
5 [nc addObserver:self selector:@selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];
6 [nc addObserver:self selector:@selector(keyboardDidHide:) name:UIKeyboardDidHideNotification object:nil];
7}
1private func initKeyboardNotificationEvents() {
2 let nc = NotificationCenter.default
3 nc.addObserver(self, selector: #selector(keyboardWillShow), name: UIWindow.keyboardWillShowNotification, object: nil)
4 nc.addObserver(self, selector: #selector(keyboardWillHide), name: UIWindow.keyboardWillHideNotification, object: nil)
5 nc.addObserver(self, selector: #selector(keyboardDidShow), name: UIWindow.keyboardDidShowNotification, object: nil)
6 nc.addObserver(self, selector: #selector(keyboardDidHide), name: UIWindow.keyboardDidHideNotification, object: nil)
7}

Execute the initKeyboardNotificationEvents inside the viewDidLoad method.

OBJECTIVEC
SWIFT
[self initKeyboardNotificationEvents];
self.initKeyboardNotificationEvents()

Add required methods:

OBJECTIVEC
SWIFT
1- (void)keyboardWillShow:(NSNotification *)notification {
2 if (!self.keyboardShown) {
3 CGFloat keyboardHeight = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height;
4 [self adjustHeight:YES withHeight:keyboardHeight];
5 }
6}
7
8- (void)keyboardWillHide:(NSNotification *)notification {
9 if (self.keyboardShown) {
10 CGFloat keyboardHeight = [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue].size.height;
11 [self adjustHeight:NO withHeight:keyboardHeight];
12 }
13}
14
15- (void)keyboardDidShow:(NSNotification *)notification {
16 self.keyboardShown = YES;
17}
18
19- (void)keyboardDidHide:(NSNotification *)notification {
20 self.keyboardShown = NO;
21}
22
23- (void)adjustHeight:(Boolean)show withHeight:(CGFloat)height {
24 CGFloat bottomStackHeight = self.bottomStackHeight.constant;
25 CGFloat changeInHeight = (height - bottomStackHeight) * (show ? KEYBOARD_SHOW_MULTIPLIER : KEYBOARD_HIDE_MULTIPLIER);
26 self.bottomConstraint.constant += changeInHeight;
27}
1@objc private func keyboardWillShow(notification : Notification) {
2 if !self.isKeyboardShown {
3 let keyboardHeight = getKeyboardHeight(notification: notification)
4 if let keyboardHeight = keyboardHeight {
5 self.adjustHeight(withHeight: keyboardHeight, show: true)
6 }
7 }
8}
9
10@objc private func keyboardWillHide(notification : Notification) {
11 if self.isKeyboardShown {
12 let keyboardHeight = getKeyboardHeight(notification: notification)
13 if let keyboardHeight = keyboardHeight {
14 self.adjustHeight(withHeight: keyboardHeight, show: false)
15 }
16 }
17}
18
19private func getKeyboardHeight(notification: Notification) -> CGFloat? {
20 let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
21 return keyboardFrame?.cgRectValue.height
22}
23
24@objc private func keyboardDidShow(notification : Notification) {
25 self.isKeyboardShown = true
26}
27
28@objc private func keyboardDidHide(notification : Notification){
29 self.isKeyboardShown = false
30}
31
32private func adjustHeight(withHeight height: CGFloat, show: Bool){
33 let bottomStackHeight = self.bottomStackHeight.constant
34 let changeInHeight = (height - bottomStackHeight) * CGFloat(show ? ViewController.KEYBOARD_SHOW_MULTIPLIER : ViewController.KEYBOARD_HIDE_MULTIPLIER)
35 self.bottomConstraint.constant += changeInHeight
36}

Methods keyboardDidShow and keyboardDidHide are used to set the keyboardShown field with a Boolean value holding information about a current keyboard state. Inside a keyboardWillShow and a keyboardWillHide methods, the keyboard height is adjusted with the value of a keyboard frame rectangle height.

Optionally implement a single tap on the map event, where you can hide keyboard if it is visible:

OBJECTIVEC
SWIFT
1- (void)mapView:(TTMapView *_Nonnull)mapView didSingleTap:(CLLocationCoordinate2D)coordinate {
2 [self.view endEditing:YES];
3}
1func mapView(_ mapView: TTMapView, didSingleTap coordinate: CLLocationCoordinate2D) {
2 self.searchBar.endEditing(true)
3}

Add a UISearchBarDelegate protocol declaration to the ViewController inside the ViewController.m file and a SearchBar outlet as property inside. Connect your outlet property with the SearchBar from the Main.storyboard file.

OBJECTIVEC
SWIFT
@property(weak,nonatomic) IBOutlet UISearchBar *searchBar;
@IBOutlet private var searchBar: UISearchBar!

Designate a current ViewController object as a delegate for the newly added searchBar property by adding this line inside the initUIViews method:

OBJECTIVEC
SWIFT
self.searchBar.delegate = self;
self.searchBar.delegate = self

This will allow you to receive and react to the searchBar event notifications. Implement a method searchBarSearchButtonClicked to be executed whenever a search bar search button is pressed.

In the next step add implementation of the method executed whenever the search bar button is clicked.

OBJECTIVEC
SWIFT
1- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
2 if (self.fullRoute) {
3 [self searchAlongTheRoute:self.searchBar.text];
4 }
5}
6- (void)searchAlongTheRoute:(NSString *)searchText {
7 TTAlongRouteSearchQuery *alongRouteSearchQuery = [[[[TTAlongRouteSearchQueryBuilder alloc] initWithTerm:searchText withRoute:self.fullRoute withMaxDetourTime:1000] withLimit:10] build];
8 [self.alongRouteSearch searchWithQuery:alongRouteSearchQuery];
9}
1func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
2 if self.fullRoute != nil, let searchText = self.searchBar.text {
3 self.searchAlongTheRoute(searchText)
4 } else {
5 self.displayMessage("Long press on the map to choose departure and destination points")
6 }
7}
8
9private func searchAlongTheRoute(_ searchText: String) {
10 let query = TTAlongRouteSearchQueryBuilder(term: searchText, withRoute: self.fullRoute, withMaxDetourTime: ViewController.MAX_DETOUR_TIME)
11 .withLimit(ViewController.SEARCH_RESULTS_LIMIT)
12 .build()
13 self.alongRouteSearch.search(with: query)
14}

The searchAlongTheRoute function in this object executes a search query for the provided search term along the route visible on the map.

Add a completedWithResponse method executed when a response is received from the TomTom search module. Make sure to declare that your ViewController class conforms to the TTAlongRouteSearchDelegate protocol.

OBJECTIVEC
SWIFT
1- (void)search:(TTAlongRouteSearch *)search completedWithResponse:(TTAlongRouteSearchResponse *)response {
2 [self.tomtomMap.annotationManager removeAllAnnotations];
3 [self.positionsPoisInfo removeAllObjects];
4
5 for (TTAlongRouteSearchResult *result in response.results) {
6 NSString *markerString = [NSString stringWithFormat:@"%@, %@", result.poi.name, result.address.freeformAddress];
7 [self createAndDisplayMarkerAtPosition:result.position withAnnotationImage:TTAnnotation.defaultAnnotationImage andBalloonText:markerString];
8 }
9 [self.tomtomMap zoomToAllAnnotations];
10}
1func search(_ search: TTAlongRouteSearch, completedWith response: TTAlongRouteSearchResponse) {
2 self.tomtomMap.annotationManager.removeAllAnnotations()
3 self.positionsPoisInfo.removeAll()
4 for result in response.results {
5 guard result.poi != nil && result.address.freeformAddress != nil else {
6 continue
7 }
8 let markerText = "\(result.poi!.name), \(result.address.freeformAddress!)"
9 self.createAndDisplayMarkerAtPosition(result.position, withAnnotationImage: TTAnnotation.defaultAnnotationImage(), andBaloonText: markerText)
10 }
11 self.tomtomMap.zoomToAllAnnotations()
12}

The createAndDisplayMarkerAtPosition method then adds a map marker in the position returned by the search query, including the name and address of the POI.

Adding custom marker balloons

Add a new User Interface View file named CustomAnnotationView. Open the newly added CustomAnnotationView.xib file and configure the size of the View component inside this file.

Add labels where a point of interest name and an address can be displayed. Additionally add a button to include point of interest marker to the route displayed on map.

Select all views and add a layout constraints to align items.

Now create a new Cocoa Touch Class file named CustomAnnotationView as a subclass of UIView . Make sure that your CustomAnnotationView conforms to a TTCalloutView protocol by modifying a CustomAnnotationView file.

OBJECTIVEC
SWIFT
1#import <UIKit/UIKit.h>
2#import <TomTomOnlineSDKMaps/TomTomOnlineSDKMaps.h>
3@interface CustomAnnotationView : UIView <TTCalloutView>
4
5@end
1import UIKit
2import TomTomOnlineSDKMaps
3
4class CustomAnnotationView : UIView & TTCalloutView {
5}

Configure a custom class for the main View component in the CustomViewController.xib file.

Inside the CustomAnnotationView file add a WayPointAddedDelegate protocol with a setWayPoint method defined inside. The class implementing this protocol receives notifications from a button click inside the marker balloon. Additionally define properties and methods inside the CustomAnnotationView.

OBJECTIVEC
SWIFT
1#import <UIKit/UIKit.h>
2#import <TomTomOnlineSDKMaps/TomTomOnlineSDKMaps.h>
3
4@protocol WayPointAddedDelegate
5
6- (void) setWayPoint:(TTAnnotation* )annotation;
7
8@end
9
10@interface CustomAnnotationView : UIView <TTCalloutView>
11
12@property(nonatomic, strong) TTAnnotation *annotation;
13@property(strong, nonatomic) IBOutlet UILabel *poiName;
14@property(strong, nonatomic) IBOutlet UILabel *poiAddress;
15@property(strong, nonatomic) id myDelegate;
16- (IBAction)addWayPoint:(id)sender;
17
18@end
1import UIKit
2import TomTomOnlineSDKMaps
3
4protocol WayPointAddedDelegate {
5 func setWayPoint(_ annotation: TTAnnotation!)
6}
7
8class CustomAnnotationView : UIView & TTCalloutView {
9
10 @IBOutlet var poiName: UILabel!
11 @IBOutlet var poiAddress: UILabel!
12 var annotation: TTAnnotation!
13 var myDelegate: WayPointAddedDelegate!
14
15 @IBAction func addWayPoint(_ sender: Any) {
16 }
17
18}

Connect outlets with previously added labels and the addWayPointmethod for the Touch up inside action performed on the add waypoint button.

Now add an implementation of the CustomAnnotationView class inside the CustomAnnotationView.m file

OBJECTIVEC
SWIFT
1#import "CustomAnnotationView.h"
2
3@implementation CustomAnnotationView
4
5- (instancetype)initWithCoder:(NSCoder *)aDecoder {
6 self = [super initWithCoder:aDecoder];
7 return self;
8}
9
10- (IBAction)addWayPoint:(id)sender {
11 [self.myDelegate setWayPoint:self.annotation];
12}
13
14@end
1import UIKit
2import TomTomOnlineSDKMaps
3
4class CustomAnnotationView : UIView & TTCalloutView {
5
6 @IBOutlet var poiName: UILabel!
7 @IBOutlet var poiAddress: UILabel!
8 var annotation: TTAnnotation!
9 var myDelegate: WayPointAddedDelegate!
10
11 required init?(coder: NSCoder) {
12 super.init(coder: coder)
13 }
14
15 @IBAction func addWayPoint(_ sender: Any) {
16 self.myDelegate.setWayPoint(self.annotation)
17 }
18}

When a class implements the WayPointAddedDelegate protocol it must define a setWayPoint method which is executed whenever a button inside marker balloon is clicked.

Import CustomAnnotationView.h inside the ViewController.m file.

#import"CustomAnnotationView.h"

Add the WayPointAddedDelegate protocol declaration to the ViewController class and implement a setWaypoint method

OBJECTIVEC
SWIFT
1- (void)setWayPoint:(TTAnnotation *)annotation {
2 self.wayPointPosition = [annotation coordinate];
3 [self.tomtomMap.annotationManager deselectAnnotation];
4 [self drawRouteWithDeparture:self.departurePosition andDestination:self.destinationPosition andWayPoint:self.wayPointPosition];
5}
1func setWayPoint(_ annotation: TTAnnotation!) {
2 self.wayPointPosition = annotation.coordinate
3 self.tomtomMap.annotationManager.deselectAnnotation()
4 self.drawRouteWithDeparture(wayPoint: self.wayPointPosition)
5}

In the last step overwrite default behavior of the viewForSelectedAnnotation method where custom annotation is created for each point of interest along a route.

OBJECTIVEC
SWIFT
1- (UIView <TTCalloutView> *)annotationManager:(id<TTAnnotationManager>)manager viewForSelectedAnnotation:(TTAnnotation *)selectedAnnotation {
2 NSString *selectedCoordinatesString = [self coordinatesToString:selectedAnnotation.coordinate];
3 if ([selectedCoordinatesString isEqualToString:[self coordinatesToString:self.departurePosition]]) {
4 return [[TTCalloutOutlineView alloc] init];
5 } else {
6 return [[TTCalloutOutlineView alloc ] initWithUIView:[self createCustomAnnotation:selectedAnnotation]];
7 }
8}
9
10- (UIView <TTCalloutView> *)createCustomAnnotation:(TTAnnotation *)selectedAnnotation {
11 CustomAnnotationView <TTCalloutView> *customAnnotation = [[NSBundle.mainBundle loadNibNamed:@"CustomAnnotationView" owner:self options:nil] firstObject];
12 NSArray *annotationStringArray = [self.positionsPoisInfo[[self coordinatesToString:selectedAnnotation.coordinate]] componentsSeparatedByString:@","];
13 customAnnotation.annotation = selectedAnnotation;
14 customAnnotation.poiName.text = annotationStringArray[0];
15 customAnnotation.poiAddress.text = annotationStringArray[1];
16 customAnnotation.myDelegate = self;
17 return customAnnotation;
18}
19- (NSString *)coordinatesToString:(CLLocationCoordinate2D)coords {
20 return [NSString stringWithFormat:@"%@,%@", [@(coords.latitude) stringValue], [@(coords.longitude) stringValue]];
21}
1func annotationManager(_ manager: TTAnnotationManager, viewForSelectedAnnotation selectedAnnotation: TTAnnotation) -> UIView & TTCalloutView {
2 let selectedCoordinate = self.coordinatesToString(selectedAnnotation.coordinate)
3 if self.departurePosition != nil && selectedCoordinate == self.coordinatesToString(self.departurePosition!) {
4 return TTCalloutOutlineView()
5 } else {
6 return TTCalloutOutlineView(uiView: self.createCustomAnnotation(selectedAnnotation))
7 }
8}
9
10private func createCustomAnnotation(_ selectedAnnotation: TTAnnotation) -> UIView {
11 let customAnnotation = Bundle.main.loadNibNamed("CustomAnnotationView", owner: nil, options: nil)?.first as! CustomAnnotationView
12 let annotationStringArray = self.positionsPoisInfo[ self.coordinatesToString(selectedAnnotation.coordinate)]?.components(separatedBy: ",")
13 customAnnotation.annotation = selectedAnnotation
14 customAnnotation.poiName.text = annotationStringArray?[0] ?? ""
15 customAnnotation.poiAddress.text = annotationStringArray?[1] ?? ""
16 customAnnotation.myDelegate = self
17 return customAnnotation
18}
19
20private func coordinatesToString(_ coordinate: CLLocationCoordinate2D) -> String {
21 return "\(coordinate.latitude),\(coordinate.longitude)"
22}

Inside the createCustomAnnotation method a CustomAnnotationView object is created, and labels for this object are populated with the information about a found point of interest. Next this annotation is displayed for each marker visible on the map.

Now you should have a fully working application where you can:

  • Display a map.
  • Create a route between 2 points.
  • Display points of interest.
  • Add a single POI to your route.

The additional styling, shortcut buttons, and help screen in the application screenshots are not a part of this tutorial. You can find them, along with all the icons and images used in this tutorial, in the application posted on Github.

Summary

This tutorial explained how to create a sample application that searches for and displays points of interest(POIs) along a route, then recalculates the route to include one of those POIs.

This application can be extended with other TomTom Maps SDK functions, such as displaying information about traffic and travel distances.

Happy coding!

Example application

The full application, including additional layout changes and improvements, is visible below. At the bottom of the screen there are three optional buttons that can be used for quick searches for gas stations, restaurants, and ATMs. There is also a help button in the top right corner along with a clear button to remove the route and any markers from the map.