Tutorial: Making a Kids Maps App for my Daughter using Google Maps

Called How Long Until We’re There?

Andy O'Sullivan
8 min readNov 14, 2022

I have just created & published to the Apple App Store a new iOS app called How Long Until We’re There? for my seven year old daughter, and also as entry to a Google Maps Platform hackathon (https://devpost.com/software/kids-car-map-app) Entering the hackathon was a bonus, but the real value here was creating something to stop my kid asking a hundred times a car journey … “How Long Until We’re There?!”.

The “Business Problem”

My daughter is too clever to ask “Are We There Yet?” when she knows we’re not, but she loves to know how long it’s going to be. She’s not placated with “soon” or “not for hours!”, she wants to know the exact minutes! If I’ve got Google Maps open on my phone I can tell her each time she asks, or I can give her my phone and she can check herself — but as amazing as Google Maps is, it isn’t a kids’ app.

So — I decided to make her a simple, clear, fun app that:

  • Will show her where she is on a map.
  • Allow her to search for an address (she can read!) and see where that is on the map.
  • See how many minutes it’s going to be before she gets there.

In terms of non-functional requirements:

  • Easy to use.
  • Colourful and fun!

The Tech Stack

I’ve an iPhone so decided on a native iOS straight away — Swift with UIKit.

To show the map on screen:

To provide the auto-complete search of addresses:

To compute the time to travel by car between the user’s location and the destination address:

Note that the Routes API “is currently in Preview (pre-GA). Pre-GA products and features might have limited support, and changes to pre-GA products and features might not be compatible with other pre-GA versions” so use at your own discretion!

I’m using the API for the time computation as it’s not available in a SDK yet. (I think! New to these APIs!)

High Level Steps To Build the App

I not going to cover every single thing in this tutorial, but keep it high level. You can also see all the code on Github here. You’ll need a knowledge of iOS development, so I’m presuming you know your way around Swift etc.

Step 1 — Create a new iOS app

I used Swift and UIKit

Step 2 — Register for the Google Maps products you need

You’ll need a Google Cloud Platform (GCP) account — https://cloud.google.com/ and once all set up, you’ll need to create a new Project. Within the project you then enable whichever services you need. I used:

  • Maps SDK for iOS
  • Places SDK for iOS
  • Routes API

Check the pricing etc — there is a certain amount of free credits each month, but you’ll get billed for going over, so be careful!

Step 3 — Get API keys for all services you’re using

You can generate keys in the GCP console and restrict them to your app using the bundle identifier.

Step 4 — Install the SDKs using Cocoa-pods in your Xcode project

You can manually install the SDKs, but cocoa-pods is much easier. You’re probably better off referring to the official docs on how to do it — https://developers.google.com/maps/documentation/ios-sdk/config but a few details to help you out:

  • Create a pod file in your project source directory:
source 'https://github.com/CocoaPods/Specs.git'

platform :ios, '13.0'

target 'YourAppName' do
pod 'GoogleMaps', '7.1.0'
pod 'GooglePlaces', '7.1.0'
end

and run:

pod install

Then remember to only open the project from now on using the .xcworkspace file.

Step 5 — set up the SDKS in your AppDelegate

Once the SDKS are installed correctly, edit the AppDelegate file:

import UIKit
import GoogleMaps
import GooglePlaces

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
GMSServices.provideAPIKey("your_maps_api_key")
GMSPlacesClient.provideAPIKey("your_places_api+key")
return true
}

Note the two imports and the two declarations with API keys.

Step 6 — Set up your main ViewController

Add a UIView to your main ViewController and set its Class as GMSMapView:

As well as the GMSMapView, you can see I’ve added buttons and label, and then I added corresponding Outlets and Actions in the ViewController.swift file for all the buttons.

Step 7 — Set up location services

You’ll need to get the users location — this may seem a bit complex, but it’s mainly boilerplate.

Firstly — add to your info.plist an entry for requesting location permission:

or in code:

    <key>NSLocationWhenInUseUsageDescription</key>
<string>Location information is used to get time to destination.</string>
</dict>
</plist>

Then in your ViewController:

import CoreLocation

and

class ViewController: UIViewController, CLLocationManagerDelegate,

and

func getUserLocation(){
let manager = CLLocationManager()
locationManager.delegate = self
if (manager.authorizationStatus == .authorizedWhenInUse)
|| (manager.authorizationStatus == .authorizedAlways) {
print("location authorised")
locationManager.startUpdatingLocation()
}else{
print("location not authorised")
locationManager.requestWhenInUseAuthorization()
}
}

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
locationManager.delegate = nil
locationManager.stopUpdatingLocation()
let location = locations.first!

print("latitude: \(location.coordinate.latitude), longitude: \(location.coordinate.longitude)")

let newLocation = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
let newCam = GMSCameraUpdate.setTarget(newLocation)
mapView.animate(with: newCam)
mapView.animate(toZoom: 12)

marker.position = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
marker.icon = UIImage(named: smallCharacterImageArray[selectedCharacter])
marker.map = mapView

sourceCoordinates = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)

if didUserAskForTime {
let calls = NetworkCalls()
calls.delegate = self
//need to get new source before getting distance to destination
if hasDestinationBeenEntered {
calls.postRoute(startCoordinates: sourceCoordinates, destinationCoordinates: destinationCoordinates)
}
}
}

func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .authorizedWhenInUse:
locationManager.startUpdatingLocation()
break
default:
print("location permission not granted")
}
}

Those functions allow you to get the latitide and longitude of the user — and in didUpdateLocations there’s additional code to add a marker to the map at the location (using whichever character the user has selected):

let newLocation = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
let newCam = GMSCameraUpdate.setTarget(newLocation)
mapView.animate(with: newCam)
mapView.animate(toZoom: 12)

marker.position = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
marker.icon = UIImage(named: smallCharacterImageArray[selectedCharacter])
marker.map = mapView

and there’s also code in there to make the call the Routes API if needed:

if hasDestinationBeenEntered {
calls.postRoute(startCoordinates: sourceCoordinates, destinationCoordinates: destinationCoordinates)
}

I’m not going to explain the code in detail — this tutorial is more to give you the main concepts, and you can work on them from there!

Step 8 — Using Places Autocomplete

We want the user to be able to type in an address for the destination — we use the Place SDK for this.

Here’s the code for when the user hits the “Where am I going” button:

@IBAction func onWhereAmIGoingButton(_ sender: Any) {
if (hasDestinationBeenEntered) {
addMarkerAndMoveToCoordinates()
}else{
let autocompleteController = GMSAutocompleteViewController()
autocompleteController.delegate = self

// Specify the place data types to return.
let fields: GMSPlaceField = GMSPlaceField(rawValue: UInt(GMSPlaceField.name.rawValue) |
UInt(GMSPlaceField.placeID.rawValue))
autocompleteController.placeFields = fields

// Specify a filter.
let filter = GMSAutocompleteFilter()
filter.types = ["address"]
autocompleteController.autocompleteFilter = filter

// Display the autocomplete view controller.
present(autocompleteController, animated: true, completion: nil)
}
}

The “GMSAutocompleteViewController” is the search screen that pops up:

Note also the “address” filter — this limits the search results to addresses (as oppose to establishments). This is a limitation, as it won’t show businesses — I presume there is workaround / configuration to allow both, but I didn’t discover it yet!

There’s more code needed to hook all this up:

func getLatLongFromPlaceId () {
// Specify the place data types to return.
let fields: GMSPlaceField = GMSPlaceField(rawValue: UInt(GMSPlaceField.coordinate.rawValue) |
UInt(GMSPlaceField.placeID.rawValue))

placesClient?.fetchPlace(fromPlaceID: placeID, placeFields: fields, sessionToken: nil, callback: {
(place: GMSPlace?, error: Error?) in
if let error = error {
print("An error occurred: \(error.localizedDescription)")
return
}
if let place = place {
print("The selected place coordinate is: \(place.coordinate)")
self.destinationCoordinates = place.coordinate
self.hasDestinationBeenEntered = true
self.addMarkerAndMoveToCoordinates()
}else{
print("hmm, here");
}
})
}

and

extension ViewController: GMSAutocompleteViewControllerDelegate {

// Handle the user's selection.
func viewController(_ viewController: GMSAutocompleteViewController, didAutocompleteWith place: GMSPlace) {
print("Place name: \(place.name ?? "placename")")
print("Place ID: \(place.placeID ?? "")")
print("Place attributions: \(String(describing: place.attributions))")
self.placeID = place.placeID ?? "nil"
self.getLatLongFromPlaceId()
self.placeName = (place.name ?? "")
dismiss(animated: true, completion: nil)
}

func viewController(_ viewController: GMSAutocompleteViewController, didFailAutocompleteWithError error: Error) {
// TODO: handle the error.
print("Error: ", error.localizedDescription)
}

// User canceled the operation.
func wasCancelled(_ viewController: GMSAutocompleteViewController) {
dismiss(animated: true, completion: nil)
}

// Turn the network activity indicator on and off again.
func didRequestAutocompletePredictions(_ viewController: GMSAutocompleteViewController) {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
}

func didUpdateAutocompletePredictions(_ viewController: GMSAutocompleteViewController) {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}

}

Step 9— Set up the Routes API

The Routes API is used to compute the time to drive between the source location and the destination location. I created a separate NetworkCalls.swift file, here it is in its entirety:

//
// NetworkCalls.swift
//
// Created by Andy O'Sullivan on 06/10/2022.
//

import Foundation
import CoreLocation

protocol NetworkServiceDelegate {
func didCompleteRouteRequest(result: String)
}
class NetworkCalls {

var delegate: NetworkServiceDelegate?

func postRoute(startCoordinates: CLLocationCoordinate2D, destinationCoordinates: CLLocationCoordinate2D){
print("postRoute called:")
print(startCoordinates)
print(destinationCoordinates)

let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration)
let url = URL(string: "https://routes.googleapis.com/directions/v2:computeRoutes?key=your_routes_api_key&fields=routes.duration,routes.distanceMeters")
var request : URLRequest = URLRequest(url: url!)
request.addValue("text/plain", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"


let options = ["origin":["location":["latLng":["latitude": startCoordinates.latitude, "longitude": startCoordinates.longitude ]]]
,
"destination":["location":["latLng":["latitude": destinationCoordinates.latitude, "longitude": destinationCoordinates.longitude ]]]]

do {
request.httpBody = try JSONSerialization.data(withJSONObject: options, options: []) // pass dictionary to data object and set it as request body
print("all good: \(String(describing: request.httpBody))")
} catch let error {
print(error.localizedDescription)
print("json error")
}

let jsonData = try! JSONSerialization.data(withJSONObject: options, options: [])
let dataString = String(data: jsonData, encoding: .utf8)!
print(dataString)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
// Do something...
guard let httpResponse = response as? HTTPURLResponse, let receivedData = data
else {
print("error: not a valid http response")
return
}
switch (httpResponse.statusCode) {
case 200:
//success response.
print("200 success! \(String(describing: String(data: receivedData, encoding: .utf8)))")
if data != nil {
do{
//here dataResponse received from a network request
let jsonResponse = try JSONSerialization.jsonObject(with:
receivedData, options: []) as? [String:Any]



print((jsonResponse!["routes"]! as AnyObject)) //Response result
let routes = jsonResponse!["routes"]
print(routes!)


if let results = jsonResponse!["routes"] as! [Any]?{
for result in results {
print(result)
if let locationDictionary = result as? [String : Any] {
print(locationDictionary["duration"]!)
let temp = locationDictionary["duration"]
self.delegate?.didCompleteRouteRequest(result: temp as! String)
}

}

}
} catch let parsingError {
print("Error", parsingError)
}

}
//self.delegate?.didCompleteRouteRequest(result: "whatevs")
break
case 400:
print("400 no! \(String(describing: String(data: receivedData, encoding: .utf8)))")
break
default:
print("default no! \(httpResponse.statusCode) all: \(httpResponse)")
break
}
}
task.resume()
}

}

It’s basically a POST to the Routes API (you’ll have to plug in your own key), using the start and destination co-ordinates.

Conclusion

Ok — so we’ve covered off the main concepts and shown you a lot of the code, enough we think to get you going.

The official documentation is great and where I figured it all out — https://developers.google.com/maps/documentation/

You can see the completed app here:

https://github.com/andyosullivan/kids-maps-app-tutorial

and it’s open-source so enjoy! Note that the icons in the sample project come from https://icons8.com/ and to use their icons you need either a license or to attribute their usage — more details on their site.

Finally — let me know what you think, you can get me on https://twitter.com/LeMarquisOfAndy or https://www.linkedin.com/in/andy-o-sullivan-9673019/ or in the comments below!

--

--