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

Called 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.

  • 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.
  • Easy to use.
  • Colourful and fun!

The Tech Stack

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

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.

  • Maps SDK for iOS
  • Places SDK for iOS
  • Routes API
  • 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
pod install
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
}
    <key>NSLocationWhenInUseUsageDescription</key>
<string>Location information is used to get time to destination.</string>
</dict>
</plist>
import CoreLocation
class ViewController: UIViewController, CLLocationManagerDelegate,
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")
}
}
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
if hasDestinationBeenEntered {
calls.postRoute(startCoordinates: sourceCoordinates, destinationCoordinates: destinationCoordinates)
}
@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)
}
}
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");
}
})
}
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
}

}
//
// 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()
}

}

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.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store