Up to iOS 5, Google Maps was an integral part of iOS and was the mapping engine used by all iOS devices. With the release of iOS 6 in 2012, Apple made a dramatic change and replaced Google Maps with an in-house mapping engine.
Just a few months later, Google released its own standalone Google Maps app for iOS, along with the Google Maps iOS SDK for developers.
There are benefits and drawbacks to both MapKit and the Google Maps iOS SDK, but this tutorial will walk you through implementing Google Maps into your apps to see for yourself how well it can work in your geolocating apps.
In this tutorial, you’ll build an app called Feed Me, which gets the user’s current location and searches for nearby places to eat, drink, or even go grocery shopping. The results will be presented in-app using the Google Maps iOS SDK.
This tutorial assumes some familiarity with Swift and iOS programming. If you’re new to Swift, or to iOS programming altogether, you might want to check out some of our other tutorials before continuing.
This tutorial also requires familiarity with Cocoapods. You must have Cocoapods installed in order to follow this tutorial. To learn more about Cocoapods, check out this tutorial by Joshua Greene, published right here on the site.
This tutorial uses Xcode 6.4 and iOS 8.4, and requires knowledge of Auto Layout, Size Classes, and of course, Swift.
Getting Started
Download the Feed Me Starter Project This project already uses Cocoapods, so open it using theFeed Me.xcworkspace file.
Take a look around to get familiar with the project. The important elements to notice are:
- MapViewController.swift: This is the main view controller of this project, and you’ll only be working with this controller in this tutorial.
- GoogleDataProvider.swift: This is a wrapper class for making Google API calls. You’ll review the methods contained within later in the tutorial.
- GooglePlace.swift: This is a model for place results returned from Google.
- MarkerInfoView.swift: This is a subclass of
UIView
that displays details of places. It comes with a matching xib file.
Before you start coding it’s a good idea to see how the app works. Build and run your app; you’ll see the following screen appear:
Right now all you’ll see is a blank screen with a pin in the middle. Press the action button on the right side of the navigation bar to see the
TypesTableViewController
screen like so:
That’s all there is to see in the app at the moment — it’s up to you to add some magic!
Creating API Keys
The first thing you’ll need are some API keys for the Google Maps SDK and the Google APIs you’ll be using. If you don’t already have a Google account, create one (they’re free!) and log in to theGoogle Developers Console.
Click on Create Project, name your project Feed Me, and click Create:
Select APIs & auth and then APIs from the left pane menu. Search and enable these APIs:
- Google Maps SDK for iOS
- Google Places API Web Service
Now switch to the Enabled APIs tab, and verify that your screen now looks like the following:
Select Credentials under APIs & auth in the left pane menu. Click Create new key, and then click iOS key to create the Google Maps SDK key:
Enter the starter project’s bundle identifier (
tutorial.feedme.com
) and click Create:
Create another key, only this time choose Server key to create the Places API key. Leave the text box empty and click Create. You should now have two boxes with server and iOS keys, like this:
You’ll use both keys in a moment, but first you’ll add the actual Google Maps iOS SDK.
Adding the SDK
Open Podfile (in the Pods project) and add the following, right above
end
:pod 'GoogleMaps'
|
Next, Open Terminal and navigate to the directory that contains your Feed Me project by using the
cd
command:cd ~/Path/To/Folder/Containing/Feed Me |
Enter the following command to install the Google Maps iOS SDK:
pod install
|
You should see output similar to the following:
Downloading dependencies Installing GoogleMaps (1.10.1) Using SwiftyJSON (2.2.0) Generating Pods project Integrating client project |
You now have GoogleMaps in your project. Doesn’t Cocoapods make life a whole lot easier? :]
Before you start writing some actual code, you need to create a bridging header and add Google Maps to it. This is because Google Maps is written in Objective-C, and you need to make it available for your Swift code.
Select the Feed Me folder in the Navigator and select File\New\File…, then choose theiOS\Source\Objective-C File template. Name the file whatever you like, you’ll delete this file in a moment, and then save it. When saving the file, Xcode offers to create an Objective-C bridging header file for you like so:
Click Yes and Xcode will create the bridging header file and add it to your project. Delete the Objective-C files (.h & .m) you just created as you no longer need then.
Open the newly created Feed Me-Bridging-Header.h and add the following line of code to the bottom of the file:
#import <GoogleMaps/GoogleMaps.h>
|
Note: While you should be able to start using the Google Maps SDK at this stage, there is one more step you need to take because of a bug in Cocoapods. In the Project Navigator, select the Feed Me project at the top. Choose the Feed Me target, select the Build Settings tab, and in Other Linker Flags add -ObjC as shown below:
Hopefully this issue will be resolved in a future update to Cocoapods, and the tutorial will be updated accordingly.
The Google Maps iOS SDK is now available in your Swift app — it’s finally time to write some code! :]
Open AppDelegate.swift and replace it’s contents with the following:
import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? // 1 let googleMapsApiKey = "YOUR_GOOGLE_IOS_API_KEY" func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { // 2 GMSServices.provideAPIKey(googleMapsApiKey) return true } } |
There are two new elements here:
- A constant to hold your Google iOS API key. Replace
YOUR_GOOGLE_IOS_API_KEY
with the Google iOS API key you created earlier. - Your app will instantiate Google Maps services with the API Key using the
GMSServices
class methodprovideAPIKey()
.
Next, open Main.storyboard to bring up Interface Builder. Bring up the Object Library by selecting the third tab in the view toolbar — Utilities — and then select the third tab in the library toolbar — Object Library — as shown in the screenshot below:
Locate the MapViewController scene and drag a simple
UIView
from the Object Library to the approximate center of the MapViewController
’s view. Change the view’s background color to light gray. Next, open the Document Outline using Editor\Show Document Outline and re-order the view hierarchy so that the object tree looks like this:
To turn this simple
UIView
into a GMSMapView
, select the view you just added and open theIdentity Inspector by selecting the third tab from the left in the Utilities toolbar. Change the view’s Class to GMSMapView
, as shown in the screenshot below:
Your MapViewController scene should now look like this:
Next, you’ll need to add some constraints to make the map fill the entire screen. Select Map Viewin the Document Outline and then choose the second button from the left in the bottom right of the Interface Builder window — the Pin button. Ensure that Constrain to margins is unchecked — this ensures that the map will fill all the available space on the screen — and add 0 (zero) space constraints from the top, left, bottom and right of the superview.
Your Pin editor should look like this:
Click on Add 4 Constraints to add the constraints to the map view. To update the frame, select the button to the right of the Pin button — the Resolve Auto Layout Issues button — and selectUpdate Frames.
Your MapViewController scene should look like the following, where the gray area represents the GMSMapView:
Before you build and run the project, add an
IBOutlet
for the map view. To do that, bring up the Assistant Editor by selecting the second tab in the Editor toolbar:
Select the map view in Interface Builder, hold down the Ctrl key and drag a line from the map view to MapViewController.swift. A popup will appear; set the connection type to Outlet and the name to
mapView
. Keep the Type as GMSMapView
, and click Connect:
This will create a
GMSMapView
property in MapViewController.swift and automatically hook it up in Interface Builder. Build and run your project; you should now see a map, like so:
You’re now using the Google Maps iOS SDK in your app — but you can do more than show a basic map, right? :]
Getting the Location
Feed Me is all about finding places near the user, and you can’t do that without getting the user’s location.
First, open MapViewController.swift and add the following property:
let locationManager = CLLocationManager() |
This will add and instantiate a
CLLocationManager
property named locationManager
.
Next, find
viewDidLoad()
and add these two lines to the bottom:locationManager.delegate = self locationManager.requestWhenInUseAuthorization() |
This will make
MapViewController
the delegate of locationManager
and request access to the user’s location.
Next, open the Feed Me project at the top of the Navigator, select the Feed Me target and go to the Info tab, select the first line in the Custom iOS Target Properties section and click the + icon to add a new row.
Enter
NSLocationWhenInUseUsageDescription
as the key, choose String
for the type, and enter the following text as the value:By accessing your location, this app can find you a good place to eat.
When you’re finished, it should look something like this:
Note: For more information on the changes to
CLLocationManager
in iOS 8, check out theclass documentation.
Before you can run the app, you need to make
MapViewController
conform to theCLLocationManagerDelegate
protocol.
Add the following extension to the bottom of MapViewController.Swift:
// MARK: - CLLocationManagerDelegate //1 extension MapViewController: CLLocationManagerDelegate { // 2 func locationManager(manager: CLLocationManager!, didChangeAuthorizationStatus status: CLAuthorizationStatus) { // 3 if status == .AuthorizedWhenInUse { // 4 locationManager.startUpdatingLocation() //5 mapView.myLocationEnabled = true mapView.settings.myLocationButton = true } } // 6 func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) { if let location = locations.first as? CLLocation { // 7 mapView.camera = GMSCameraPosition(target: location.coordinate, zoom: 15, bearing: 0, viewingAngle: 0) // 8 locationManager.stopUpdatingLocation() } } } |
Taking each numbered comment in turn:
- You create a
MapViewController
extension that conforms toCLLocationManagerDelegate
. locationManager(_:didChangeAuthorizationStatus:)
is called when the user grants or revokes location permissions.- Here you verify the user has granted you permission while the app is in use.
- Once permissions have been established, ask the location manager for updates on the user’s location.
GMSMapView
has two features concerning the user’s location:myLocationEnabled
draws a light blue dot where the user is located, whilemyLocationButton
, when set totrue
, adds a button to the map that, when tapped, centers the map on the user’s location.locationManager(_:didUpdateLocations:)
executes when the location manager receives new location data.- This updates the map’s camera to center around the user’s current location. The
GMSCameraPosition
class aggregates all camera position parameters and passes them to the map for display. - Tell
locationManager
you’re no longer interested in updates; you don’t want to follow a user around as their initial location is enough for you to work with.
Build and run your app; once it loads you’ll be prompted with an alert, asking for location permissions. Tap on Allow:
You should now see a map centering around your location. Scroll the map and tap the Locatebutton and the map will center back to your location like so:
Implementing Geocoding
Now that you have the user’s location, it would be nice if you could show the street address of that location. Google has an object that does exactly that:
GMSGeocoder
. This takes a simple coordinate and returns a readable street address.
First, you’ll need to add some UI to present the address to the user.
Open Main.storyboard and add a
UILabel
to the MapViewController scene. Make sure you add the label to the MapViewController
view, and not the GMSMapView
.
Next, open the Attributes Inspector, and give the label the following attributes:
- Set Alignment to center.
- Set Lines to 0. Surprisingly, this lets the label take up as many lines as it needs to fit the text. Go figure! :]
- Set Background to white with 85% opacity.
The label’s Attributes Inspector and the scene’s Object Tree should look like this when done:
Finally, add to the label left, bottom and right constraints of 0 as shown below:
This pins the label to the bottom of the screen and stretches it over the entire width of the screen. Update the frames and continue.
Your storyboard scene should look like the following:
Next, create an outlet for the label. Select the Assistant Editor, and ctrl+drag from the label in the Document Outline to MapViewController.swift. Set the connection type to Outlet, the name to
addressLabel
and click Connect.
This adds a property to your
MapViewController
that you can use in your code:@IBOutlet weak var addressLabel: UILabel! |
Add the method below to MapViewController.swift:
func reverseGeocodeCoordinate(coordinate: CLLocationCoordinate2D) { // 1 let geocoder = GMSGeocoder() // 2 geocoder.reverseGeocodeCoordinate(coordinate) { response, error in if let address = response?.firstResult() { // 3 let lines = address.lines as! [String] self.addressLabel.text = join("\n", lines) // 4 UIView.animateWithDuration(0.25) { self.view.layoutIfNeeded() } } } } |
Once again, here’s what each commented section does:
- Creates a
GMSGeocoder
object to turn a latitude and longitude coordinate into a street address. - Asks the geocoder to reverse geocode the coordinate passed to the method. It then verifies there is an address in the response of type
GMSAddress
. This is a model class for addresses returned by theGMSGeocoder
. - Sets the text of the
addressLabel
to the address returned by the geocoder. - Once the address is set, animate the changes in the label’s intrinsic content size.
You’ll want to call this method every time the user changes their position on the map. To do so you’ll use
GMSMapViewDelegate
.
Add another extension to the bottom of MapViewController.swift as follows:
// MARK: - GMSMapViewDelegate extension MapViewController: GMSMapViewDelegate { } |
This will declare that
MapViewController
conforms to the GMSMapViewDelegate
protocol.
Next, add the following line of code to
viewDidLoad()
:mapView.delegate = self |
This makes
MapViewController
the map view’s delegate.
Finally, add the following method to the newly added extension:
func mapView(mapView: GMSMapView!, idleAtCameraPosition position: GMSCameraPosition!) { reverseGeocodeCoordinate(position.target) } |
This method is called each time the map stops moving and settles in a new position, where you then make a call to reverse geocode the new position and update the
addressLabel
‘s text.
Build and run your app; you’ll see the address of your current location — real or simulated — pop up at the bottom of the screen:
Notice anything wrong with this picture?
Solution Inside | Show |
---|---|
Fortunately,
GMSMapView
provides a very simple solution for this: padding. When padding is applied to the map, all of the visual elements will be placed according to that padding.
Head back to
reverseGeocodeCoordinate(_:)
, and make these changes to the animation block:// 1 let labelHeight = self.addressLabel.intrinsicContentSize().height self.mapView.padding = UIEdgeInsets(top: self.topLayoutGuide.length, left: 0, bottom: labelHeight, right: 0) UIView.animateWithDuration(0.25) { //2 self.pinImageVerticalConstraint.constant = ((labelHeight - self.topLayoutGuide.length) * 0.5) self.view.layoutIfNeeded() } |
This does two things:
- Prior to the animation block, this adds padding to the top and bottom of the map. The top padding equals the navigation bar’s height, while the bottom padding equals the label’s height.
- Updates the location pin’s position to match the map’s padding by adjusting its vertical layout constraint.
Build and run your app again; this time the Google logo and locate button will move to their new position once the label becomes visible:
Move the map around; you’ll notice that the address changes every time the map settles to a new position. Let’s add a visual effect to dampen the address changing.
Add the following method to the
GMSMapViewDelegate
extension:func mapView(mapView: GMSMapView!, willMove gesture: Bool) { addressLabel.lock() } |
This method is called every time the map starts to move. It receives a
Bool
that tells you if the movement originated from a user gesture, such as scrolling the map, or if the movement originated from code. You call the lock()
on the addressLabel
to give it a loading animation.
When there’s a
lock()
, there must also be an unlock()
. Add the following to the top ofreverseGeocodeCoordinate(_:)
‘s closure:func reverseGeocodeCoordinate(coordinate: CLLocationCoordinate2D) { let geocoder = GMSGeocoder() geocoder.reverseGeocodeCoordinate(coordinate) { response, error in //Add this line self.addressLabel.unlock() //Rest of response handling } } |
Note: For the full implementation of the
lock()
and unlock()
, check outUIViewExtensions.swift.
Build and run your app; as you scroll the map you should see a loading animation on the address label like so:
Finding Something to Eat
Now that the map is set up and you have the user’s location in hand, it’s time to get this user fed!
You’ll use the Google Places API to search for places to eat and drink around the user’s location.Google Places API is a free web service API you can use to query to find establishment, geographic locations, or other points of interest near any given point.
Unfortunately, Google does prohibit direct usage of this API from mobile apps, and allows usage only from server-side apps. This means that in order to use this service, you will have to use your own server to fetch the results, and query that server from your iOS app.
Since writing the server is out of scope for this tutorial, you can find a ready-made server in the starter project download, which is written in Dart and was kindly provided by Brett Morgan, Developer Advocate at Google. Brett has since open-sourced the server, and the repo can be found on GitHub here.
Let’s go over the steps for running the server:
- Download the server code.
- To install Dart, you’ll use Homebrew. Homebrew is a package manager for mac that allows simple installation of various packages. If you do not already have it installed, open Terminaland run:
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
- When Homebrew is done installing, run the following command in Terminal:
brew tap dart-lang/dart
- Install Dart by running the following command in Terminal:
brew install dart
- Next, locate the path where you downloaded the server code to, and open it in terminal:
cd [PATH_TO_SERVER_CODE]
- Install the server code by running the following in Terminal:
pub install
- Finally, run the server with the following command:
pub run bin/main.dart -k [YOUR_API_KEY] -p 10000
ReplaceYOUR_API_KEY
with the server key you created earlier
Note: For more information, checkout the readme.md file in places_api_key_proxy folder you downloaded in step 1.
To check that querying the Google Places API works, click here. You should see the query result in a format of JSON. If you open your Terminal window you’ll see some logs:
Note: You must keep the terminal window open while running your app. If you close it, follow steps 5 & 7 to run the server again.
Note: The current configuration will allow you to run the server from the iPhone simulator. To run it from a device, locate GoogleDataProvider.swift, search for
localhost
and replace it with your mac’s ip address. In addition, make sure you’re connected to the same WiFi network on both your mac and iOS device.
Now that you can query places from Google, let’s go back to the Feed Me project.
Google Maps iOS SDK provides you with the
Google Maps iOS SDK provides you with the
GMSMarker
class to mark locations on a map. Each marker object holds a coordinate and an icon image and renders on the map when added.
For this app you’ll need some more info on each marker, so you’ll need to create a subclass of
GMSMarker
.
Create a new Cocoa Touch Class, name it
PlaceMarker
and make it a subclass of GMSMarker
. Ensure you choose Swift as the language for this file.
Replace the contents of PlaceMarker.swift with the following:
class PlaceMarker: GMSMarker { // 1 let place: GooglePlace // 2 init(place: GooglePlace) { self.place = place super.init() position = place.coordinate icon = UIImage(named: place.placeType+"_pin") groundAnchor = CGPoint(x: 0.5, y: 1) appearAnimation = kGMSMarkerAnimationPop } } |
This is a relatively straightforward bit of code:
- Add a property of type
GooglePlace
to thePlaceMarker
. - Declare a new designated initializer that accepts a
GooglePlace
as its sole parameter and fully initializes aPlaceMarker
with a position, icon image, anchor for the marker’s position and an appearance animation.
Next, add two more properties to MapViewController.swift as follows:
let dataProvider = GoogleDataProvider() let searchRadius: Double = 1000 |
You’ll use
dataProvider
to make calls to the Google Places API, and searchRadius
to determine how far from the user’s location to search for places.
Add the following method to MapViewController.swift:
func fetchNearbyPlaces(coordinate: CLLocationCoordinate2D) { // 1 mapView.clear() // 2 dataProvider.fetchPlacesNearCoordinate(coordinate, radius:searchRadius, types: searchedTypes) { places in for place: GooglePlace in places { // 3 let marker = PlaceMarker(place: place) // 4 marker.map = self.mapView } } } |
Let’s go over what you just added:
- Clear the map of all markers.
- Use
dataProvider
to query Google for nearby places around thesearchRadius
, filtered to the user’s selected types. - Enumerate through the results returned in the completion closure and create a
PlaceMarker
for each result. - Set the marker’s map. This line of code is what tells the map to render the marker.
Here’s the $64,000 question — when do you want to call this method?
First, the user can reasonably expect to see places nearby when the app launches.
Locate
locationManager(_:didUpdateLocations:)
and add the following line of code at the end, within the if let
statement:fetchNearbyPlaces(location.coordinate) |
Second, the user has the ability to change the types of places to display on the map, so you’ll need to update the search results if the selected types change.
Locate
typesController(_:didSelectTypes:)
and add the following line of code to the end:fetchNearbyPlaces(mapView.camera.target) |
Finally, you’ll need to give the user the option to fetch new places when their location changes.
Open Main.storyboard and drag a
UIBarButtonItem
from the Object Library to the left side of the MapViewController
’s navigation bar. Change the button’s Identifier to Refresh, as shown below:
Select the Assistant Editor and ctrl+drag from the Refresh button toMapViewController.swift. Choose Action and name the method
refreshPlaces
. Replace the contents of the newly added method with the following:@IBAction func refreshPlaces(sender: AnyObject) { fetchNearbyPlaces(mapView.camera.target) } |
Build and run your project; you’ll see location pins popping up around the map. Change the search types in the
TypesTableViewController
and see how the results change:
Note: If you do not see any of the locations appear, you can debug the issue by openingGoogleDataProvider.swift and underneath the line of code that serializes the json object, print the error to the console by adding the following:
println(json["error_message"])
.
All these markers sure add some color to the map, but they’re not much use without additional info to give the user some details on the pinned location.
Add the following method to the
GMSMapViewDelegate
extension in MapViewController.swift:func mapView(mapView: GMSMapView!, markerInfoContents marker: GMSMarker!) -> UIView! { // 1 let placeMarker = marker as! PlaceMarker // 2 if let infoView = UIView.viewFromNibName("MarkerInfoView") as? MarkerInfoView { // 3 infoView.nameLabel.text = placeMarker.place.name // 4 if let photo = placeMarker.place.photo { infoView.placePhoto.image = photo } else { infoView.placePhoto.image = UIImage(named: "generic") } return infoView } else { return nil } } |
This method is called each time the user taps a marker on the map. If you return a view, then it pops up above the marker. If
nil
is returned, nothing happens. How does that happen?- You first cast the tapped marker to a
PlaceMarker
. - Next you create a
MarkerInfoView
from its nib. TheMarkerInfoView
class is aUIView
subclass that comes with the starter project for this tutorial. - Then you apply the place name to the
nameLabel
. - Check if there’s a photo for the place. If so, add that photo to the info view. If not, add a generic photo instead.
Before you run the app, you want to make sure the location pin doesn’t cover the info window. Add the following method to the
GMSMapViewDelegate
extension:func mapView(mapView: GMSMapView!, didTapMarker marker: GMSMarker!) -> Bool { mapCenterPinImage.fadeOut(0.25) return false } |
This method simply hides the location pin when a marker is tapped. The method returns
false
to indicate that you don’t want to override the default behavior — to center the map around the marker — when tapping a marker.
Obviously, the pin needs to re-appear at some point. Add the following to the end of
mapView(_:willMove:)
:if (gesture) { mapCenterPinImage.fadeIn(0.25) mapView.selectedMarker = nil } |
This checks if the movement originated from a user gesture; if so, it un-hides the location pin using the
fadeIn(_:)
method. Setting the map’s selectedMarker
to nil will remove the currently presented infoView
.
Finally, add the following method to the
GMSMapViewDelegate
extension:func didTapMyLocationButtonForMapView(mapView: GMSMapView!) -> Bool { mapCenterPinImage.fadeIn(0.25) mapView.selectedMarker = nil return false } |
This method runs when the user taps the Locate button; the map will then center on the user’s location. Returning
false
again indicates that it does not override the default behavior when tapping the button.
Build and run your app; select a marker and you’ll see the location pin fade out. Scrolling the map closes the
infoView
and brings the pin back:
That’s it, you’ve done it! You now have a fully functioning Google Maps app. :]
Where To Go From Here?
Keep in mind that before you’re able to run this finished project, you will need to insert your API keys as you did at the beginning of this tutorial.
Our purpose here is not to convince you that Google Maps SDK is better than Apple’s own MapKit; however, here’s a few pros and cons of Google Maps:
Pros
- Frequent Updates to the SDK by Google.
- Uniform experience for cross platform (iOS and Android) apps.
- Google Maps are often more detailed, especially outside the United States. For example:
Cons
- Unlike Google Maps, MapKit is native to iOS, which means it’s always synced with iOS and works with Swift out of the box.
- Lack of stability in comparison to MapKit.
- MapKit has a much better integration with CoreLocation and CoreAnimation.
This tutorial only showed you the basics of what the Google Maps SDK can do. There’s much more to learn; you should definitely check out the full documentation for more cool features the SDK has to offer.
Among the other things you can do with Google Maps are showing directions, indoor maps, overlays, tile layers, Street View, and advanced integration with the Google Maps app. For brownie points, try to use some of these features to enhance the Feed Me app.
Thanks again to Google Developer Advocates Brett Morgan and Todd Kerpelman, both of whom worked with me to find the best way to implement the Google Places API into this tutorial.
If you have any questions, tips, or just wanna show off your cool mapping app, feel free to post about it in the discussion below!
0 comments:
Post a Comment