How to use the Google Places Autocomplete API with Google Maps SDK on iOS

With the release of Apple maps as a part of the iOS 6, developers had to roll away from all the benefits google maps was offering to the detriment of pure Apple maps SDK.

That is said, Google still has a powerful map engine for iOS that you can use to empower your app. The google maps SDK for iOS offers plenty of great API to work with, one of them is the Google Places API.

In this tutorial, you will learn how to integrate the Google Maps SDK in your iOS project, and how to take advantage of the great Place Autocomplete service in order to build your own address locator map app.

Along the way, you gonna work with Google forward geocoding API to locate the suggested addresses and POI right on the map.

Without further ado, let’s Google map it!

To start off, download the starter project here. Open it with Xcode and run it in the simulator.

The user interface is split in two parts: the search bar where you will type in addresses and POI names and the map view holder where to show the Google map with the marker of the suggested place returned by Google API.

The starter user interface, UISearchController

In just few minutes, you will implement all the stuff you need to make your app a real address/POI locator πŸ™‚

Integrate the Google Maps SDK and get the API Key:

In order to use the Google Maps SDK for iOS, you need to follow some steps to get the SDK integrated into our Xcode project. These steps include installing the SDK as a pod using the cocoapods dependency manager.

I can explain the steps here in detail, however, since Google has made a straightforward, step by step guide, I will ask you to follow it instead.

Note: You don’t have to finish all the steps in that guide, just be sure that steps 1 to 5 are completed successfully. This include getting an API key to work with πŸ™‚

I am waiting for the API Key

Follow the steps using the Xcode starter project you just downloaded. And when configuring the cocoapods Podfile, make sure to make that from within the project directory as explained in the guide.

Important Note: While following the guide steps, make sure that the bundle identifier of the project (in this case com.medigarage.PlacesLookup) is listed in the credentials screen while generating the API Key. To verify, visit the following link, go to your project, then select APIs & auth from the left menu, and then select Credentials. If the bundle identifier does not figure in the list of authorised apps, then you may want to add it πŸ™‚

Here is a screenshot of mine :

Authorised app bundle to be granted access to google API

After following the steps, you will use the .xcworkspace file to open the project from this time onwards.

Installing Google Maps SDK For iOS as a pod with cocoapods

So now, I will assume you also got your API Key which may look something like this :

AIzaSyBdVl-cTICSwYKrZ95SuvNw7dbMuDt1KG0

Congratulations, now you are good to go πŸ™‚

The first thing to do is to expose the API key to your project code, so that you can be granted access to the Google Maps SDK features through the code. The best place to do this is in the application:didFinishLaunchingWithOptions: application delegate method.

Select AppDelegate.swift file from the Project navigator view and place the following import at the top of the file:

import GoogleMaps

Next, locate the application:didFinishLaunchingWithOptions: method, and copy the following statement inside ( just before the return statement):

GMSServices.provideAPIKey("YOUR_API_KEY")

Obviously, you need to change the content between the quotes with the API Key that Google generated for you.

Now that the API Key is provided and exposed to all your app files, you can work with the Google package of API from within the code.

Remember the search button you saw in storyboard from the starter project ? Basically, you gonna show a search screen where the user can type in some text to look for addresses and POI.

The content you are going to present on the click of the search icon is managed by an instance of UISearchController class. If you are new to UISearchController, this class will mainly define an interface that manages the presentation of a search bar combined with a search results controller’s content.

Did I say a search results controller?

Well, yes indeed. The search results controller will be useful to hold the display of instant results returned from Google Places API each time the user is typing in the search bar. In other words, while user is typing, we need a controller to manage the content that you gonna display instantly after each typed character.

The following animation illustrates what I mean πŸ™‚

The autocomplete service in the Google Places API for iOS

Let’s implement the search results controller. Select “File\New\File” from the menu (or simply cmd+N). Choose “iOS\Source\Cocoa Touch Class” from the templates screen.

Select the file template in Xcode. Cocoa Touch Class as an example

Name the class file “SearchResultsController”, and make sure it’s a subclass of UITableViewController, as shown in the screenshot below.

Name the class in Xcode. UITableViewController subclass as an example

Switch to the Project navigator view, select SearchResultsController.Swift file to load it in the editor, and make the following variables declarations (just under the class name):

var searchResults: [String]!
var delegate: LocateOnTheMap!

The searchResults variable will hold all the predicted places that google API will return while you search. While the delegate variable is a protocol reference that you gonna implement in other class file.

Place the following code right above the class name:

protocol LocateOnTheMap{
    func locateWithLongitude(lon:Double, andLatitude lat:Double, andTitle title: String)
}

The protocol above declares a method with three parameters, this method will be implemented in the main view controller where the map view is presented in order to load the address/POI location.

Always in the same class, locate the viewDidLoad method and implement the following code inside (right after the super.viewDidLoad call):

self.searchResults = Array()
self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cellIdentifier")

You just allocated an empty array for the searchResults variable to work with later. And registered the current class as the one to use when creating table cells.

Remember this controller is a UITableViewController subclass, that means it will manage a table view object that you are going to use to show the places and addresses names. It also comes with a predefined table view data source protocol methods by default.

Let’s modify these protocol methods to make them work with the table view.

Locate the numberOfSectionsInTableView method and change its return value to 1, as below:

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
       return 1
}

Next, change the tableView:numberOfRowsInSection: method code to the following:

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.searchResults.count
}

Now, uncomment the tableView:cellForRowAtIndexPath: method and change its code to look like below:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("cellIdentifier", forIndexPath: indexPath)

    cell.textLabel?.text = self.searchResults[indexPath.row]
    return cell
}

You are done with the data source protocol methods, now the table view object knows how many sections it has, and how many rows it has to draw.

Let’s finish up by implementing a UITableViewDelegate protocol method that will be notifying the delegate each time the user select a row in the table view.

Place the following code before the class closing bracket:

override func tableView(tableView: UITableView,
        didSelectRowAtIndexPath indexPath: NSIndexPath){
   // 1
   self.dismissViewControllerAnimated(true, completion: nil)
   // 2
   let correctedAddress:String! = self.searchResults[indexPath.row].stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.symbolCharacterSet())
   let url = NSURL(string: "https://maps.googleapis.com/maps/api/geocode/json?address=\(correctedAddress)&sensor=false")

   let task = NSURLSession.sharedSession().dataTaskWithURL(url!) { (data, response, error) -> Void in
   // 3
   do {
       if data != nil{
          let dic = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableLeaves) as!  NSDictionary

          let lat = dic["results"]?.valueForKey("geometry")?.valueForKey("location")?.valueForKey("lat")?.objectAtIndex(0) as! Double
          let lon = dic["results"]?.valueForKey("geometry")?.valueForKey("location")?.valueForKey("lng")?.objectAtIndex(0) as! Double
          // 4
          self.delegate.locateWithLongitude(lon, andLatitude: lat, andTitle: self.searchResults[indexPath.row] )
       }
    }catch {
       print("Error")
    }
}
// 5
task.resume()
}

Let’s breakdown the code above:

// 1: Here, you dismissed the presented search results controller. Keep in mind that once you start typing in the search bar, a table view controller will be presented on the screen. And once you click on a table row, this content should be dismissed to show the selected address/POI on the map.

// 2: Next, you changed the selected address string in a sort to be able to inject it as a parameter in the url of the google geocoder API. The stringByAddingPercentEncodingWithAllowedCharacters will replace all white spaces inside the address string with percent encoded characters. You then constructed the API url to request, and proceeded an NSURLSession task through the asynchronous dataTaskWithURL method of the NSURLSession great class.

// 3: Since the requested Google API can throw errors, it’s important to handle the returned response inside a do-catch statement. That’s what you did here. If the response is a valid JSON, then the code will retrieve the latitude and longitude of the address.

// 4: Here you called the delegate method locateWithLongitude:andLatitude:andTitle: which will proceed to the map location work effectively. You will implement this method right away.

// 5: After you create the NSURLSession task, it’s important to call its resume method to start it immediately.

Build the project, no errors ? Good job πŸ™‚

Select ViewController.swift file from the Project navigator view. Now you gonna implement the protocol method you defined earlier, along with other stuff.

Let’s start by importing the google maps library in order to be able to use it from the class scope. Copy the line below at the top of the class file:

import GoogleMaps

Next, place the following variables declaration right below the class name:

var searchResultController:SearchResultsController!
var resultsArray = [String]()
var googleMapsView:GMSMapView!

The first variable is a reference of the SearchResultsController class that you implemented earlier. resultsArray is an array of strings that will store all the predicted places returned from the Google Places API, and googleMapsView is a reference of the google map view that will be drawn on the screen.

Now, implement the viewDidAppear method like below (copy all the code above inside the class):

override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)
    self.googleMapsView =  GMSMapView(frame: self.mapViewContainer.frame)
    self.view.addSubview(self.googleMapsView)
    searchResultController = SearchResultsController()
    searchResultController.delegate = self
}

The code above will instantiate a google map view object with the same frames as its container and add it to the root view. It also constructed an object for the SearchResultsController class and set the current class as its delegate.

Setting this class as the delegate for the SearchResultsController class requires two additional steps, the first is to make this class conform to the LocateOnTheMap protocol, and the second is to implement the method defined in that protocol, that is locateWithLongitude:andLatitude:andTitle.

So let’s go ahead and make the relevant changes for this. Modify the class declaration with the following:

class ViewController: UIViewController, LocateOnTheMap

Next, place the following protocol method implementation before the class closing bracket:

func locateWithLongitude(lon: Double, andLatitude lat: Double, andTitle title: String) {

dispatch_async(dispatch_get_main_queue()) { () -> Void in
 let position = CLLocationCoordinate2DMake(lat, lon)
 let marker = GMSMarker(position: position)

 let camera  = GMSCameraPosition.cameraWithLatitude(lat, longitude: lon, zoom: 10)
 self.googleMapsView.camera = camera

 marker.title = title
 marker.map = self.googleMapsView
}
}

The protocol method above will be called from within the SearchResultsController class to show the selected address on the map, it basically uses the longitude/latitude arguments to construct a marker with its position, then adjust the camera zoom level and assign a title to the marker.

Note: All the code inside the above method is done explicitly from within the main thread. This is because the caller of this method was running in a background thread, and since all UI work (including the map stuff) should be performed on the main queue, that what explain the switch to the UI thread before drawing to the map πŸ™‚

Next, locate the showSearchController action method and implement the following code inside:

let searchController = UISearchController(searchResultsController: searchResultController)
searchController.searchBar.delegate = self
self.presentViewController(searchController, animated: true, completion: nil)

The action code above will instantiate a UISearchController object, and assign the searchResultController object you created in the viewDidLoad method as the results holder for the search controller. The search controller manages a search bar, so you set the current class to become the delegate of that search bar because you gonna implement a UISearchBarDelegate protocol method right away. Finally you presented the search controller on the main screen.

You are almost done, change the class declaration to the following to make this class conform to the UISearchBarDelegate protocol:

class ViewController: UIViewController,UISearchBarDelegate, LocateOnTheMap

Cool, build and run the project, and click on the search icon to reveal the presented search controller.

One missing thing to implement is the communication between the Google Places API (including its autocomplete service) and your app. This communication should be performed each time the user is typing in the search bar. So go ahead and implement the following search bar delegate protocol method before the closing bracket of the class:

func searchBar(searchBar: UISearchBar,
        textDidChange searchText: String){

let placesClient = GMSPlacesClient()
placesClient.autocompleteQuery(searchText, bounds: nil, filter: nil) { (results, error:NSError?) -> Void in
self.resultsArray.removeAll()
if results == nil {
    return
}
for result in results!{
 if let result = result as? GMSAutocompletePrediction{
    self.resultsArray.append(result.attributedFullText.string)
 }
}
self.searchResultController.reloadDataWithArray(self.resultsArray)
}
}

The code above will mainly initialise a place client object to be able to communicate with the Google Places API, it then invokes the autocompleteQuery API from the Google Maps SDK to look for predicted places each time based on the typed text in the search bar. Once the results are returned from the server, they are appended into an array and passed as a parameter to a method of the searchResultController object to reload the table view with the list of places and addresses.

The reloadDataWithArray method is not implemented yet, so let’s implement it to finish up.

Switch back to the SearchResultsController.swift file class and copy the following code inside, just before the class closing bracket:

func reloadDataWithArray(array:[String]){
    self.searchResults = array
    self.tableView.reloadData()
}

The method above, once called, will refresh the table view inside the search results view with all the content from the searchResults data source array.

That’s it folks, build and run the project and look for your favorite starbucks. Enjoy your work πŸ™‚

Where to go from here?

As usual, you can download the final project for this tutorial here.

You just used the API from the most powerful mapping services in the world. The set of data provided to you from Google Places API is the same used by Google itself.

In this tutorial, you took advantages from two powerful API services from the Google Maps SDK for iOS: The Google Places engine and the Place Autocomplete API, in addition to the Google forward geocoder webservice.

There is more to know about Google Maps SDK for iOS, its arsenal is full of reliable services that worth learning.

If you got any question about this tutorial, feel free to join the forum discussion here. Also, feel free to leave your comments below, I’d love to hear from you!

Malek
iOS developer with over than 11 years of extensive experience working on several projects with different sized startups and corporates.

46 Comments

  1. Thanks for this tutorial but i m getting an error –
    rror Domain=com.google.places.ErrorDomain Code=-9 “The operation couldn’t be completed. Server request failed.” UserInfo=0x7fb22340 {NSUnderlyingError=0x7fb21480 “The operation couldn’t be completed. (com.google.places.server.ErrorDomain error -1.)”, NSLocalizedFailureReason=Server request failed.}
    fatal error: unexpectedly found nil while unwrapping an Optional value

    Please help me out.

  2. thanks..but still i am getting nil value of results. I do not understand why i am getting nil value?? Please help me to get rid of this problem.

  3. Actually i am printing error before if block of results, i am getting error given below-
    Error Domain=com.google.places.ErrorDomain Code=-9 “The operation couldn’t be completed. Server request failed.” UserInfo=0x79e89890 {NSUnderlyingError=0x7e789920 “The operation couldn’t be completed. (com.google.places.server.ErrorDomain error -1.)”, NSLocalizedFailureReason=Server request failed.}

  4. Now i have solved following issue but search list is not displaying…cellforrowAtindexPath not called on reload data!!!!

  5. Hey Malek. Thanks much for the tutorial, it’s awesome. I am getting this error message:

    *** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘must pass a class of kind UITableViewCell’ ***

    Any thoughts on why?

  6. Hi Tristan, My bet is that you are not registering the UITableViewCell class, most likely you are registering another class. Make sure to change the self.tableView.registerClass statement in the SearchResultsController class to the following (as I explained in the tutorial):
    self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: “cellIdentifier”)

  7. Hi Malek! Thank you very much this is the best tutorial ever!! One thing to ask…I would like to customize search result screen..so what i did was

    1) Create a TableViewController in storyboard

    2) Assigned a custom class (search result class) to the above controller

    However, it doesn’t seem like they are connected. Could you please help me out on this?

  8. Hi David πŸ™‚
    I believe you tried to instantiate the table view controller directly from its class (SearchResultsController()) and not from storyboard, which may run you in troubles. If you create a custom UITableViewController in storyboard, you need to make sure of the following points:
    – Set the class as the custom class for the UITableViewController in the Identity inspector.
    – Specify a Storyboard ID for the UITableViewController in the Identity Inspector (for example “SearchController”). This is important in order to instantiate it from the code.
    – Select the prototype cell from the table then switch to the Attributes Inspector, and set an ID in the Identifier property (example: “CellID”). This is important in order to dequeue the right cell in the datasource.
    – In the cellForRowatIndexPath, use the same Identifier you set for the cell in the storyboard (“CellID in this case”)
    – Now in order to instantiate the searchResultsController, you need to use the UIStoryboard from within your code, For example:

    let storyboard = UIStoryboard(name: “Main”, bundle: nil)
    searchResultController = storyboard.instantiateViewControllerWithIdentifier(“SearchController”) as! SearchResultsController

    This should work!
    Let me know how it goes. If you have any question, please feel free to ask. I am here to help πŸ™‚

  9. You rock Malek!! That was exactly what I wanted…i spent a lot of time trying to solve it…much appreciate it!

    I actually got another question πŸ™‚
    What I would like to do in my app is from ResultTableController, each cell got a segue to another table view controller and would like to pass in longitude and latitude where the controller is showing the result based on the location information passed from the ResultTableController.
    However, it seems like we are using async method (task) in didSelectRowAtIndex method and I cannot think of a good way of handling this behaviour. What I can think of is just to force the app to wait till it completes its task then perform a segue. This might be not a good solution. My explanation would be not clear. Let me know if you don’t get it. Otherwise, please share you thought on this! Thank you very much in advance.

    Regards,
    David

  10. Hi there!

    FANTASTIC tutorial – really excellent stuff!

    Mine is working, so far that pressing the search button brings up the Controller as expected, and I can type into the search bar etc. But, there are no “suggestions” popping up in the view, no matter what I type into the search bar (typing “Cali” doesn’t prompt “California, USA” for example).

    Any ideas on what is going wrong here?

    Thanks again.

    Kind Regards,
    Greg Hanley.

  11. Saw the following in my console too (perhaps has something to do with why I’m not getting suggestions in the Table?):

    “Set accountToAuthorizerBlock to be able to send authorized requests”

  12. Hey dude!

    Found the problem, didn’t have the “Google Places” API Enabled for this app! Carefully debugged, and analysed multiple error messages and BOOM!!

    How about placing multiple markers on a map, if, for example, an array/dict of Long/Lat values were given?

    Making an app where a user uploads an image, and provides a “Where” location for the image (which is exactly what I’ll use this tutorial for 😊), and all data (userId, imageURL, location (in text “London, UK” for example. Basically the selected row from these Google place suggestions.).

    As a result, id like to be able to view a map for the current logged in user, whereby a pin is created for each of their uploads, and placed on the map where they specified during the upload!

    Would be great to be able to click the marker, and open the image in an ImageView also!

    Thanks again, amazing stuff here!

  13. Hi David,
    Sorry for my late follow up! You can use closures to wait for the asynchronous task and call a completion handler with the longitude/latitude passed as arguments.
    Let me know how it goes πŸ™‚

  14. Thanks for this tutorial! It’s the best explained I could find after looking around.

    I downloaded the tutorial version of the project to try and run but wasn’t successful. I’m getting an error on the ViewController with the “import GoogleMaps” saying could not build Objective-C module GoogleMaps. I put in my API Key and updated the pod in Terminal with pod install, but still no luck. I feel like I’m forgetting something simple but can’t figure it out!

  15. Hi Ben πŸ™‚
    Please change the Google maps pod in the Podfile with the latest version as below:
    pod ‘GoogleMaps’,’~>1.13.0′
    That will fix the dependency issues. Don’t forget to run “pod install” after saving the Podfile.

  16. Please help me, When I type for any place in the search bar, table view doesn’t come with the predicted places.

  17. Hi Mustajab πŸ™‚
    Do you mean the table view is not showing data at all? or that the predicted places are not what you expect?
    Predicted suggestions are based on the string you enter, which in turn is interpreted with the Google maps “autocompleteQuery” API. There is nothing you can do regarding this.

  18. Thank for the reply Malek. The table view is not showing data at all, no predictions at all. Not a single cell of tableview.

  19. Hi, can you show your relevant code? The better way on the Forum discussion or even email me your sample Xcode project.

  20. I have already sorted it out through careful debugging. Thanks for the great tutorial.

  21. Hi Malek_T can you help me with this..
    The loop never executes.
    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)

    {

    self.dismissViewControllerAnimated(true, completion: nil)

    let correctedAddress:String! = self.searchResults[indexPath.row].stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.symbolCharacterSet())

    let url = NSURL(string: “https://maps.googleapis.com/maps/api/geocode/json?key=AIzaSyBb1-e4n7nSZmmS8Ykkl12IqAt8qKJn8-w&address=(correctedAddress)&sensor=false”)

    let task = NSURLSession.sharedSession().dataTaskWithURL(url!) { (data, response, error) -> Void in

    // This piece of code never run…..!!!

    do

    {

    if data != nil{

    let dic = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableLeaves) as! NSDictionary

    let lat = dic[“results”]?.valueForKey(“geometry”)?.valueForKey(“location”)?.valueForKey(“lat”)?.objectAtIndex(0) as! Double

    let lon = dic[“results”]?.valueForKey(“geometry”)?.valueForKey(“location”)?.valueForKey(“lng”)?.objectAtIndex(0) as! Double

    // 4

    self.delegate.locateWithLongitude(lon, andLatitude: lat, andTitle: self.searchResults[indexPath.row] )

    }

    }catch

    {

    print(“Error”)

    }

    }

    task.resume()

    }

  22. Hi Mustajab,
    What “loop” you are referring to? Can you set a breakpoint inside the “do” bloc and confirm it’s being called at all? If not, does the “catch” bloc throw any error?

  23. Hello Malek_T

    Yeah https://uploads.disquscdn.com/images/3fb5a46c521a4cff526f7e5a1f1516a50c69881552dce88890b43541ca11ef4e.png It wasn’t going in to the do bloc. I did

    self.dismissViewControllerAnimated(true, completion: nil) and then

    self.delegate.locateWithLongitude(lon, andLatitude: lat, andTitle: self.searchResults[indexPath.row] )

    So it went into the do bloc but now It is showing unusual behaviour at let lat = dic[“results”]?.valueForKey(“geometry”)?.valueForKey(“location”)?.valueForKey(“lat”)?.objectAtIndex(0) as! Double.

  24. Hi Malek, Is there a way to make the address autocomplete to start working only after say eight characters have been entered. Currently it works when the first character entered. Thanks

  25. Hi Nadarajan, yes this is easy to do. In the searchBar:textDidChange function, implement the following simple code (at the top of the function code):

    if searchText.utf16.count < 8
    {
    return
    }

    Basically, the code above will skip the function remaining processing as long as the entered text is lower than 8 characters.

  26. let lat = dic[“results”]?.valueForKey(“geometry”)?.valueForKey(“location”)?.valueForKey(“lat”)?.objectAtIndex(0) as! Double

    and same for longitude

    has error regarding anyobject

  27. Hi,
    can you reedit this tutorial to support Swift 3.
    I am getting some errors that I don’t know how to fix

  28. hi i am using your code bt in my app cellFoRowAtIndexPath method is not calling plzzz tell me the reason

  29. hello @Malek_T:disqus i am having error ! :{message:”Daily Limit for Unauthenticated Use Exceeded. Continued use requires signup.” errors:[1] code:403}, NSLocalizedFailureReason=(Daily Limit for Unauthenticated Use Exceeded. Continued use requires signup.)}}}}} please help ASAP!

  30. thanks a lot for your tutorial, great job. I have just a quick question, how do you filter the answer of google autocomplete for a specific country and type of Establishment like “restaurant” ?

  31. placeClient.autocompleteQuery(searchText, bounds: nil, filter: nil){ (results , error:NSError?) -> Void in
    results always return nil

Comments are closed.