6

I am trying to draw route between two places using Google Maps on a custom UIView but not able to get it correctly implemented. My custom view is mapViewX. I've installed google sdk using pods which includes pod 'GoogleMaps' and pod 'GooglePlaces'. I made custom-view Class as 'GMSMapView'. my code is :

    @IBOutlet weak var mapViewX: GMSMapView!

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.

    let path = GMSMutablePath()
    path.add(CLLocationCoordinate2D(latitude: 37.778483, longitude: -122.513960))
    path.add(CLLocationCoordinate2D(latitude: 37.706753, longitude: -122.418677))
    let polyline = GMSPolyline(path: path)
    polyline.strokeColor = .black
    polyline.strokeWidth = 10.0
    polyline.map = mapViewX

}

Please help!

3 Answers3

10

It works fine here. Make sure you're setting correct coordinates of GMSCameraPosition.

EDIT

To draw the route between two coordinate, use Google Maps Direction API

Something like :

    let origin = "\(37.778483),\(-122.513960)"
    let destination = "\(37.706753),\(-122.418677)"
    let url = "https://maps.googleapis.com/maps/api/directions/json?origin=\(origin)&destination=\(destination)&mode=driving&key=[YOUR-API-KEY]"

    Alamofire.request(url).responseJSON { response in
        let json = JSON(data: response.data!)
        let routes = json["routes"].arrayValue

        for route in routes
        {
            let routeOverviewPolyline = route["overview_polyline"].dictionary
            let points = routeOverviewPolyline?["points"]?.stringValue
            let path = GMSPath.init(fromEncodedPath: points!)

            let polyline = GMSPolyline(path: path)
            polyline.strokeColor = .black
            polyline.strokeWidth = 10.0
            polyline.map = mapViewX

        }
    }

For more info - Directions API Developer's Guide

  • By adding path.addCoordinate, it gives an error: " Value of type 'GMSMutablePath' has no member 'addCoordinate' " –  May 31 '17 at 12:04
  • @sam sorry I misunderstood. Edited my answer. – Muhammad Abdul Subhan May 31 '17 at 12:11
  • I added following lines below super.viewDidLoad() : let camera = GMSCameraPosition.camera(withLatitude: 37.778483, longitude: -122.513960, zoom: 8) mapViewX.animate(to: camera) But polyline draws to be a straight line, on an empty mapView, whereas I needed polyline route in terms of path from one point to another –  May 31 '17 at 12:56
  • Yeah that is the expected behaviour as you're giving two coordinates to draw a polyline. To draw a path along the route, you would be needing the `Google Maps Direction API` which will return you an array of paths of the routes. I'm editing the answer to add the basic usage of Directions API. – Muhammad Abdul Subhan May 31 '17 at 16:16
  • Hi Abdul, tried it but the routes array is getting 0 values in response –  Jun 02 '17 at 07:17
  • Worked, I just added few lines to update camera position, rest is working fine. –  Jun 02 '17 at 08:29
  • This code doesn't handle drawing polyline over the last one. – amish Jan 05 '18 at 05:45
2

To draw a route between 2 coordinates you need to make a request to the Google Maps Directions API and parse its response. Therefore you firstly need to get an API key for your request. You can get one here, by creating a project and also enabling the Google Maps Directions API in this project.

Assuming you have the Google Maps SDK installed, you need to make a request to the directions API and then parse its response. Once you have parsed the response JSON, you can create a GMSPath object. I prefer to do that with a function that has two inputs, start & end CLLocationCoordinate2D objects and that returns the GMSPath on success or an error if something failed. The code below is in Swift 3.

My class and its function look like this:

import Foundation
import CoreLocation
import GoogleMaps

class SessionManager {
    let GOOGLE_DIRECTIONS_API_KEY = "INSERT_YOUR_API_KEY_HERE"

    func requestDirections(from start: CLLocationCoordinate2D, to end: CLLocationCoordinate2D, completionHandler: @escaping ((_ response: GMSPath?, _ error: Error?) -> Void)) {
        guard let url = URL(string: "https://maps.googleapis.com/maps/api/directions/json?origin=\(start.latitude),\(start.longitude)&destination=\(end.latitude),\(end.longitude)&key=\(GOOGLE_DIRECTIONS_API_KEY)") else {
            let error = NSError(domain: "LocalDomain", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to create object URL"])
            print("Error: \(error)")
            completionHandler(nil, error)
            return
        }

        // Set up the session
        let config = URLSessionConfiguration.default
        let session = URLSession(configuration: config)

        let task = session.dataTask(with: url) { (data, response, error) in
            // Check if there is an error.
            guard error == nil else {
                DispatchQueue.main.async {
                    print("Google Directions Request Error: \((error!)).")
                    completionHandler(nil, error)
                }
                return
            }

            // Make sure data was received.
            guard let data = data else {
                DispatchQueue.main.async {
                    let error = NSError(domain: "GoogleDirectionsRequest", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to receive data"])
                    print("Error: \(error).")
                    completionHandler(nil, error)
                }
                return
            }

            do {
                // Convert data to dictionary.
                guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
                    DispatchQueue.main.async {
                        let error = NSError(domain: "GoogleDirectionsRequest", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to convert JSON to Dictionary"])
                        print("Error: \(error).")
                        completionHandler(nil, error)
                    }
                    return
                }

                // Check if the the Google Direction API returned a status OK response.
                guard let status: String = json["status"] as? String, status == "OK" else {
                    DispatchQueue.main.async {
                        let error = NSError(domain: "GoogleDirectionsRequest", code: 3, userInfo: [NSLocalizedDescriptionKey: "Google Direction API did not return status OK"])
                        print("Error: \(error).")
                        completionHandler(nil, error)
                    }
                    return
                }

                print("Google Direction API response:\n\(json)")

                // We only need the 'points' key of the json dictionary that resides within.
                if let routes: [Any] = json["routes"] as? [Any], routes.count > 0, let routes0: [String: Any] = routes[0] as? [String: Any], let overviewPolyline: [String: Any] = routes0["overview_polyline"] as? [String: Any], let points: String = overviewPolyline["points"] as? String {
                    // We need the get the first object of the routes array (route0), then route0's overview_polyline and finally overview_polyline's points object.

                    if let path: GMSPath = GMSPath(fromEncodedPath: points) {
                        DispatchQueue.main.async {
                            completionHandler(path, nil)
                        }
                        return
                    } else {
                        DispatchQueue.main.async {
                            let error = NSError(domain: "GoogleDirections", code: 5, userInfo: [NSLocalizedDescriptionKey: "Failed to create GMSPath from encoded points string."])
                            completionHandler(nil, error)
                        }
                        return
                    }

                } else {
                    DispatchQueue.main.async {
                        let error = NSError(domain: "GoogleDirections", code: 4, userInfo: [NSLocalizedDescriptionKey: "Failed to parse overview polyline's points"])
                        completionHandler(nil, error)
                    }
                    return
                }


            } catch let error as NSError  {
                DispatchQueue.main.async {
                    completionHandler(nil, error)
                }
                return
            }

        }

        task.resume()
    }
}

Then you can use that in your viewDidLoad like that:

@IBOutlet weak var mapView: GMSMapView!

override func viewDidLoad() {
    super.viewDidLoad()

    let sessionManager = SessionManager()
    let start = CLLocationCoordinate2D(latitude: 37.778483, longitude: -122.513960)
    let end = CLLocationCoordinate2D(latitude: 37.706753, longitude: -122.418677)

    sessionManager.requestDirections(from: start, to: end, completionHandler: { (path, error) in

        if let error = error {
            print("Something went wrong, abort drawing!\nError: \(error)")
        } else {
            // Create a GMSPolyline object from the GMSPath
            let polyline = GMSPolyline(path: path!)

            // Add the GMSPolyline object to the mapView
            polyline.map = self.mapView

            // Move the camera to the polyline
            let bounds = GMSCoordinateBounds(path: path!)
            let cameraUpdate = GMSCameraUpdate.fit(bounds, with: UIEdgeInsets(top: 40, left: 15, bottom: 10, right: 15))
            self.mapView.animate(with: cameraUpdate)
        }

    })

}

Hope you find it useful.

e_pie
  • 91
  • 1
  • 5
  • It crashed. Giving error: exception 'GMSThreadException', reason: 'The API method must be called from the main thread' –  Jun 02 '17 at 08:25
  • The crash is because the callback returned is not running on the main (UI) thread. I edited the answer, you shouldn't have any problem now. – e_pie Jun 04 '17 at 19:55
  • It works, thanks. but can you please explain why we needed DispatchQueue.main.async. were we not on the main thread? –  Jun 05 '17 at 18:46
  • Yes, sorry, my mistake. If you put a breakpoint within `dataTask(with:)` you'll notice that it runs on a background thread. Therefore, when you call its completionHandler, it will return on the same thread as the thread that it runs (i.e. in a background thread). However, you cannot perform UI operations (such as drawing a polyline) on a background thread. I edited my answer so the completion handler is always called on the main thread, therefore you don't need to call `DispatchQueue` within the block of `requestDirections` in the `viewDidLoad` – e_pie Jun 05 '17 at 22:17
2

Swift 4

Create global variables.

var sourceLat = 0.0
var sourceLong = 0.0
var DestinationLat = 0.0
var DestinationLong = 0.0
var startLOC = CLLocation()
var endLOC = CLLocation()

Install Pod Alamofire and SwiftJSON.

pod 'Alamofire', '~> 4.5'
pod 'SwiftyJSON'

Make a function for draw route between source and destination.

func drawPath(startLocation: CLLocation, endLocation: CLLocation)
{
    let origin = "\(startLocation.coordinate.latitude),\(startLocation.coordinate.longitude)"
    let destination = "\(endLocation.coordinate.latitude),\(endLocation.coordinate.longitude)"
    let url = "https://maps.googleapis.com/maps/api/directions/json?origin=\(origin)&destination=\(destination)&mode=driving"

    Alamofire.request(url).responseJSON { response in
        //print(response.request as Any)  // original URL request
        //print(response.response as Any) // HTTP URL response
        //print(response.data as Any)     // server data
        //print(response.result as Any)   // result of response serialization

        let json = JSON(data: response.data!)
        let routes = json["routes"].arrayValue
        print(json)
        // print route using Polyline

        DispatchQueue.global(qos: .default).async(execute: {() -> Void in
            // Do something...
            DispatchQueue.main.async(execute: {() -> Void in
               // self.hideHUD()
            })
        })
        for route in routes
        {
            let routeOverviewPolyline = route["overview_polyline"].dictionary
            let points = routeOverviewPolyline?["points"]?.stringValue
            let path = GMSPath.init(fromEncodedPath: points!)
            let polyline = GMSPolyline.init(path: path)
            polyline.strokeWidth = 4
            polyline.strokeColor =  UIColor.black
            polyline.map = self.mapViewBus

        }

    }
}

Create button action provide it source route and destination route. Giving Latitude and Longitude. After that paste the code.

 // Route Source & Destination
            self.startLOC = CLLocation(latitude: sourceLat, longitude: sourceLong)
            self.endLOC = CLLocation(latitude: DestinationLat, longitude: DestinationLong)

            drawPath(startLocation: startLOC, endLocation: endLOC)


            let marker = GMSMarker()
            marker.position = CLLocationCoordinate2D(latitude: sourceLat, longitude: sourceLong)
           // marker.icon = userImage.af_imageScaled(to: CGSize(width: 50, height: 50)).af_imageRoundedIntoCircle()
            marker.title = "Source"
            marker.map = mapViewBus


            let markerr = GMSMarker()
            markerr.position = CLLocationCoordinate2D(latitude: DestinationLat, longitude: DestinationLong)
           // markerr.icon =  washerImage.af_imageScaled(to: CGSize(width: 50, height: 50)).af_imageRoundedIntoCircle()
            markerr.title = "Desintation"
            markerr.map = mapViewBus

            let camera = GMSCameraPosition.camera(withLatitude: sourceLat, longitude: sourceLong, zoom: 14.0)
            self.mapViewBus.camera = camera
            self.mapViewBus.animate(to: camera)
Khawar Islam
  • 2,556
  • 2
  • 34
  • 56