Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

Introduction to Google Maps iOS SDK in Swift

$
0
0

GoogleMaps_Main

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 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 other tutorials previously published on this site.

This tutorial uses Xcode 6.1 with iOS 8 and requires knowledge of Auto Layout, Size Classes, and of course, Swift.

Getting Started

Download the Feed Me Starter and open it in Xcode 6.

Here are the most important elements in the project:

  • Main.storyboard: This is the UI storyboard for the project, with some very basic UI ready for your modifications. Notice that this storyboard uses Auto Layout and Size Classes.
  • MapViewController.swift: This is the main view controller of this project; there’s not much in it right now, other than a types array and navigation methods that present a type selection controller. You’ll be working with just MapViewController in this tutorial; don’t worry, by the time you’re done it will look a lot more finished!
  • TypesTableViewController.swift: This is a table view controller that lets you search for types of places to present them on a map.
  • 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.
  • UIViewExtension.swift: This is a UIView class extension — similar to a category in Objective-C — with several methods you’ll use as you make your way through the tutorial.

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:

GoogleMaps_GettingStarted_01

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:

GoogleMaps_GettingStarted_02

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 the Google Developers Console.

Click on Create Project, name your project Feed Me, accept the terms of service and click Create:

GoogleMaps_APIKeys_01
Select APIs & auth and then APIs from the left pane menu. You’ll see a long list of available APIs on the main pane. Disable the currently selected APIs, and enable the following three APIs:

  • Google Maps SDK for iOS
  • Places API
  • Directions API

Your screen should now look like the following:

GoogleMaps_APIKeys_02

Select Credentials under APIs & auth in the left pane menu. Click Create new key, and then click iOS key to create the Maps SDK key:

GoogleMaps_APIKeys_03

Enter the starter project’s bundle identifier (tutorial.feedme.com) and click Create:

GoogleMaps_APIKeys_04

Create another key, only this time choose Server key to create the Places and Directions APIs key. Leave the text box empty and click Create. You should now have two boxes with browser and iOS keys, like this:

GoogleMaps_APIKeys_05

Go back to Xcode and open GoogleDataProvider.swift. Locate the apiKey property declaration below:

let apiKey = "YOUR_API_KEY"

and replace it with the server key you created above. You’ll use the iOS key in a moment, but you need the actual Google Maps SDK key first.

Adding the Maps

Download the Google Maps SDK from the Google Maps SDK for iOS page. This tutorial uses version 1.8.1 of the SDK, so if the API has advanced by the time you read this tutorial find the version closest to 1.8.1 and use that.

Return to Xcode and drag the GoogleMaps.framwork file you downloaded into the Frameworks folder. When prompted, check Copy items if needed, make sure the Feed Me target is checked and click Finish:

GoogleMaps_AddingMaps_01

Right-click on GoogleMaps.framework in the Project Navigator and select Show In Finder. Expand the framework and go into the Resources folder. Drag the GoogleMaps.bundle from the Resources folder to your Frameworks folder in Xcode. Once again, make sure Copy items if needed and the Feed Me target are checked, then click Finish.

Next, select the Feed Me project at the top of the Project Navigator, and choose the Feed Me target. Select the Build Phases tab, and within Link Binary with Libraries add the following frameworks by clicking the + sign:

  • AVFoundation.framework
  • CoreData.framework
  • CoreLocation.framework
  • CoreText.framework
  • GLKit.framework
  • ImageIO.framework
  • libc++.dylib
  • libicucore.dylib
  • libz.dylib
  • OpenGLES.framework
  • QuartzCore.framework
  • SystemConfiguration.framework

When finished, the Frameworks folder in the Project Navigator should look like this:

GoogleMaps_AddingMaps_02

Once again, select the Feed Me project at the top of the Project Navigator, only this time, choose the Feed Me Project rather than the target. Select the Build Settings tab, and in Other Linker Flags add -ObjC as shown below:

GoogleMaps_AddingMaps_03

That last step is required because the Google Maps SDK is currently written in Objective-C; even though you won’t be writing Objective-C code, you’ll still need to link in Objective-C libraries. To use Google’s Obj-C SDK code in your Swift application, you’ll need an Objective-C Bridging Header file.

This easiest way to create a bridging header file is to add an arbitrary Objective-C file to your project and let Xcode create the bridging header for you.

Go to File \ New \ File… and select the iOS \ Source \ Objective-C file template. Name the file whatever you like — you’ll delete this file in a moment — then save it. When save the file, Xcode offers to create an Objective-C bridging header file for you like so:
GoogleMaps_AddingMaps_04

Click Yes and Xcode will create the bridging header file and add it to your project. Delete the original Objective-C file as you no longer need it.

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>

The Google Maps SDK is now available in your Swift app — it’s finally time to write some code! :]

Open AppDelegate.swift and replace its content 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: NSDictionary!) -> Bool {
    // 2
    GMSServices.provideAPIKey(googleMapsApiKey)
    return true
  }
}

There are two new elements here:

  1. There is new a constant to hold your Google iOS API key. Replace YOUR_GOOGLE_IOS_API_KEY with the Google iOS API key you created earlier.
  2. Your app will instantiate Google Maps services with the API Key using the GMSServices class method provideAPIKEY.

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:

GoogleMaps_AddingMaps_05

Locate the MapViewController scene and drag a simple UIView from the Object library to the approximate center of the MapViewController’s view. Next, open the Document Outline using Editor \ Show Document Outline and re-order the view hierarchy so that the object tree looks like this:

GoogleMaps_AddingMaps_06

To turn this simple UIView into a GMSMapView, select the view you just added and open the Identity Inspector by selecting the third tab from the left in the Utilities toolbar. Change the view’s class name from UIView to GMSMapView, as shown in the screenshot below:

GoogleMaps_AddingMaps_07

Open the Attributes Inspector by selecting the fourth tab from the left in the Utilities toolbar, and change the MapView’s background color to a light grey color. This will not affect the view during runtime, but will make the MapView more visible as you edit it in Interface Builder.

Your MapViewController scene should now look like this:

GoogleMaps_AddingMaps_08

Next, you’ll need to add some constraints to make the map fill the entire screen. Select the second button in the bottom left of the Interface Builder window — the Pin button — and add 0 (zero) space constraints from the top, left, bottom and right of the superview.

For the top margin constraint, make sure you set it from its superview and not from the top layout margin, as you want the map to go behind the navigation bar. Your Pin editor should look like this:

GoogleMaps_AddingMaps_09

Ensure that Constrain to layout margins is unchecked — this ensures that the map will fill all the available space. on the screen.

Click on Add 4 constraints to add the constraints to the map view. To update the frame, select the button on the right of the Pin button — the Resolve button — and select Update Frames.

Note: Need an Auto Layout refresher? Check out the Beginning Auto Layout tutorial on this site.

Your MapViewController scene should look like the following, where the grey area represents the GMSMapView:

GoogleMaps_AddingMaps_11

Before you build and run the project, add an IBOutlet for the MapView. To do that, bring up the Assistant Editor by selecting the second tab in the Editor toolbar:

GoogleMaps_AddingMaps_12

Select the MapView in Interface Builder, hold down the Ctrl key and drag a line from the MapView to MapViewController.swift, just above the mapCenterPinImage property. A popup will appear; set the connection type to Outlet and the name to mapView. Keep the Type as GMSMapView, and click Connect:

GoogleMaps_AddingMaps_13

This will create a GMSMapView property in MapViewController.swift and automatically hook it up to Interface Builder. The class and properties declarations in MapViewController.swift should look like the following:

class MapViewController: UIViewController, TypesTableViewControllerDelegate {
  @IBOutlet weak var mapView: GMSMapView!
  @IBOutlet weak var mapCenterPinImage: UIImageView!
  @IBOutlet weak var pinImageVerticalConstraint: NSLayoutConstraint!
  var searchedTypes = ["bakery", "bar", "cafe", "grocery_or_supermarket", "restaurant"]

Build and run your project; you should see a Google Map showing half the world like so:

GoogleMaps_AddingMaps_14

In its default mode, the MapView displays as a Normal map, but you can give the user the option to choose Satellite or Hybrid views.
Open Main.storyboard, go to the Object Library and drag a UISegmentedControl onto the MapViewController’s Navigation Bar. Ensure the segmented control is selected, go to the Attributes Inspector — the third tab — and increase the number of segments to 3.

Double click on each of the segments to change their names to Normal, Satellite, and Hybrid respectively. Your MapViewController scene should now look like this:

GoogleMaps_AddingMaps_15

Add the following method to MapViewController.swift:

@IBAction func mapTypeSegmentPressed(sender: AnyObject) {
  let segmentedControl = sender as UISegmentedControl
  switch segmentedControl.selectedSegmentIndex {
  case 0:
      mapView.mapType = kGMSTypeNormal
    case 1:
      mapView.mapType = kGMSTypeSatellite
    case 2:
      mapView.mapType = kGMSTypeHybrid
    default:
      mapView.mapType = mapView.mapType
  }
}

In the code above you check the selected segment index and use a switch block to set the new mapType accordingly.

Head back to Main.storyboard, select the Assistant Editor and Ctrl+drag from the Segmented Control to the newly added method to hook it up.

Build and run your app again; toggle between the different map types and see what the different results look like:
GoogleMaps_AddingMaps_16

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.

To request the user’s location, open MapViewController.swift and at the top of the file, make the following changes:

// 1
class MapViewController: UIViewController, TypesTableViewControllerDelegate, CLLocationManagerDelegate {
  @IBOutlet weak var mapView: GMSMapView!
  @IBOutlet weak var mapCenterPinImage: UIImageView!
  @IBOutlet weak var pinImageVerticalConstraint: NSLayoutConstraint!
  var searchedTypes = ["bakery", "bar", "cafe", "grocery_or_supermarket", "restaurant"]
  //2
  let locationManager = CLLocationManager()
 
  override func viewDidLoad() {
    super.viewDidLoad()
    //3
    locationManager.delegate = self
    locationManager.requestWhenInUseAuthorization()
  }

Here’s what’s the code above does:

  1. Declares that MapViewController conforms to the CLLocationManagerDelegate protocol.
  2. Adds a constant property named locationManager and instantiates it with a CLLocationManager object.
  3. Makes the MapViewController the delegate of locationManager and requests access to the user’s location.

Next, open the Feed Me project at the top of the Project Navigator, go to the Info tab, select the first line 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 done, it should look like this:

Screen Shot 2014-10-08 at 11.37.02 AM

Note: If you used CLLocationManager in previous iOS versions, the above will probably look new to you. Don’t worry, you haven’t been doing anything wrong. In iOS 8, Apple changed the way you request a user for their location. You now have to specifically request the permission, giving one of two options: requestWhenInUseAuthorization to get location updates while the app is in the foreground, and requestAlwaysAuthorization to get location updates while the app is in the background.

In addition, you need to explain why you need these permissions by adding the NSLocationWhenInUseUsageDescription or NSLocationAlwaysUsageDescription keys to your info.plist. The value for these keys is displayed to the user when they are prompted for location permissions.

For more information, check out the CLLocationManager class documentation.

Build and run your app; once it loads you’ll be prompted with an alert, asking for location permissions. Tap on Allow:

Allow Locations

Now that you’ve seen how to request location permissions in iOS 8, you can use these granted permissions in your app.

Add the following two CLLocationManagerDelegate methods to MapViewController.swift:

  // 1
  func locationManager(manager: CLLocationManager!, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
    // 2
    if status == .AuthorizedWhenInUse {
 
      // 3
      locationManager.startUpdatingLocation()
 
      //4
      mapView.myLocationEnabled = true
      mapView.settings.myLocationButton = true
    }
  }
 
  // 5
  func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
    if let location = locations.first as? CLLocation {
 
      // 6
      mapView.camera = GMSCameraPosition(target: location.coordinate, zoom: 15, bearing: 0, viewingAngle: 0)
 
      // 7
      locationManager.stopUpdatingLocation()
    }
  }

Taking each numbered comment in turn:

  1. You invoke CLLocationManagerDelegate when the user grants or revokes location permissions.
  2. Here you verify the user has granted you permission while the app is in use.
  3. Once permissions have been established, ask the location manager for updates on the user’s location.
  4. GMSMapView has two features concerning the user’s location: myLocationEnabled draws a light blue dot where the user is located, while myLocationButton adds a button to the map that centers the user’s location when tapped.
  5. CLLocationManagerDelegate executes when the location manager receives new location data.
  6. 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.
  7. 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 project; you’ll see a map centering around your location. Scroll the map and tap the Locate button and the map will center back to your location like so:

GoogleMaps_Location_04

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.

New Label

Next, open the Attributes Inspector, and give the label the following attributes:

  • Set the text alignment to center.
  • Set the number of lines to 0. Surprisingly, this lets the label take up as many lines as it needs to fit the text. Go figure! :]
  • Set the background color to white with 85% opacity. This will give your app a nice effect by making the map slightly visible behind the label.

The label’s Attributes Inspector and the scene’s Object Tree should look like this when done:

GoogleMaps_Geocoding_01

GoogleMaps_Geocoding_06

Finally, add to the label left, bottom and right constraints of 0 as shown below:

GoogleMaps_Geocoding_02

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:

Storyboard Scene

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:

  1. Creates a GMSGeocoder object to turn a latitude and longitude coordinate into a street address.
  2. 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 the GMSGeocoder.
  3. Sets the text of the address to the address returned by the geocoder.
  4. 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 the GMSMapView‘s delegate.

Change MapViewController‘s class declaration as follows:

class MapViewController: UIViewController, TypesTableViewControllerDelegate, CLLocationManagerDelegate, GMSMapViewDelegate

In the code above you 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 MapViewController.swift:

func mapView(mapView: GMSMapView!, idleAtCameraPosition position: GMSCameraPosition!) {
  reverseGeocodeCoordinate(position.target)
}

You call this method 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:

GoogleMaps_Geocoding_03

Notice anything wrong with this picture?

Solution Inside SelectShow>

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 the 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) * 2)
  self.view.layoutIfNeeded()
}

This does two thing:

  1. Prior to the animation block, this adds top and bottom paddings to the map. the top padding equals the navigation bar’s height, while the bottom padding equals the label’s height.
  2. Updates the location pin’s position to match the map’s padding by adjusting it’s 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:

GoogleMaps_Geocoding_04

Move the map around; you’ll notice that the address changes every time the map settles to a new position. However, this change happens very suddenly could be unsettling to your user. You’ll add another visual effect to dampen the address changing.

Add the following method to MapViewController.swift:

func mapView(mapView: GMSMapView!, willMove gesture: Bool) {
  addressLabel.lock()
}

You call the above method each time the map starts to move. It accepts a Bool that tells you if the movement originated from a user gesture, such as scrolling the map, or if the movement originated elsewhere, such as 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(), so add that call to the top of the reverseGeocodeCoordinate closure:

func reverseGeocodeCoordinate(coordinate: CLLocationCoordinate2D) {
    let geocoder = GMSGeocoder()
    geocoder.reverseGeocodeCoordinate(coordinate) { response , error in
 
      //Add this line
      self.addressLabel.unlock()
      if let address = response.firstResult() {
        let lines = address.lines as [String]
        self.addressLabel.text = join("\n", lines)
 
        let labelHeight = self.addressLabel.intrinsicContentSize().height
        self.mapView.padding = UIEdgeInsets(top: self.topLayoutGuide.length, left: 0, bottom: labelHeight, right: 0)
        UIView.animateWithDuration(0.25) {
          self.pinImageVerticalConstraint.constant = ((labelHeight - self.topLayoutGuide.length) * 2)
          self.view.layoutIfNeeded()
        }
      }
    }
  }
Note: For the full implementation of the lock() and unlock(), check out UIViewExtensions.swift.

Build and run your app; as you scroll the map you should see a loading animation on the address label like so:
GoogleMaps_Geocoding_05

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.

Note: For more information on the Google Places API, check out the Using the Google Places API With MapKit tutorial on this site. The GoogleDataProvider and GooglePlace classes that come with the starter project implement this API for the use of Feed Me.

Google Maps 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:

  1. Add a property of type GooglePlace to the PlaceMarker.
  2. Declare a new designated initializer that accepts a GooglePlace as its sole parameter and fully initializes a PlaceMarker with a position, icon image, anchor for the marker’s position and an appearance animation.

Head back to MapViewController.swift and add the following computed property that calculates the visible radius of the map:

var mapRadius: Double {
  get {
    let region = mapView.projection.visibleRegion()
    let center = mapView.camera.target
 
    let north = CLLocation(latitude: region.farLeft.latitude, longitude: center.longitude)
    let south = CLLocation(latitude: region.nearLeft.latitude, longitude: center.longitude)
    let west = CLLocation(latitude: center.latitude, longitude: region.farLeft.longitude)
    let east = CLLocation(latitude: center.latitude, longitude: region.farRight.longitude)
 
    let verticalDistance = north.distanceFromLocation(south)
    let horizontalDistance = west.distanceFromLocation(east)
    return max(horizontalDistance, verticalDistance)*0.5
  }
}

Above you use use the map’s projection to find the visible region of the map. A GMSProjection defines a mapping between coordinates on earth — CLLocationCoordinate2D — and coordinates in the map’s view — CGPoint.

You next create four CLLocation objects to represent the four basic directions. Since the height and width of the map view aren’t equal, you determine the maximum of of the horizontal and vertical distances of the visible region and return that value divided by 2.

Note: Computed properties don’t actually store a value, but instead provide a getter and an optional setter to retrieve and set other properties and values indirectly. In the above case the mapRadius property only provides a getter, and returns a calculated map radius.

Now, add another property to MapViewController.swift as follows:

let dataProvider = GoogleDataProvider()

You’ll use dataProvider to make calls to the Google Places API.

Add the following method to MapViewController.swift:

func fetchNearbyPlaces(coordinate: CLLocationCoordinate2D) {
  // 1
  mapView.clear()
  // 2
  dataProvider.fetchPlacesNearCoordinate(coordinate, radius:mapRadius, 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:

  1. Clear the map from all current markers.
  2. Use dataProvider to query Google for nearby places around the mapRadius, filtered to the user’s selected types.
  3. Enumerate through the results returned in the completion closure and create a PlaceMarker for each result.
  4. 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 locationManage(_:didUpdateLocations:) and add the following line of code at the end, within the if let statement:

fetchNearbyPlaces(location.coordinate)

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. Select the newly added bar button and go to the Attributes Inspector. Change the Identifier to refresh, as shown in the screenshot below:

GoogleMaps_Places_01

Select the Assistant Editor and Ctrl+drag from the refresh button to MapViewController.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:

GoogleMaps_Places_02

Note: If you do not see any of the locations appear, you can debug the issue by opening GoogleDataProvider.swift and underneath the line of code that serializes the json object, print the error to 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 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
  }
}

You call this method each time the user taps a marker on the map. If you receive a view, then it pops up above the marker. If nil is returned, nothing happens. How does that happen?

  1. You first cast the tapped marker to a PlaceMarker
  2. Next you create a MarkerInfoView from its nib. The MarkerInfoView class is a UIView subclass that comes with the starter project for this tutorial.
  3. Then you apply the place name to the nameLabel.
  4. 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.

Build and run your app; choose one of the markers and you’ll see the map center around the marker, which is the default behaviour when selecting markers. A small info window pops above the marker with the place details:

GoogleMaps_Places_03

Notice that when you select a marker, the location pin hovers above it. That doesn’t look too great.

To correct this, add the following method to MapViewController.swift:

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 mapViewWillMove:

if (gesture) {
  mapCenterPinImage.fadeIn(0.25)
  mapView.selectedMarker = nil
}

This checks if the movement originated from a user gesture; if so, it unhides the location pin using the fadeIn method. Setting the map’s selectedMarker to nil will remove the currently presented infoView.

Next, add the following method to MapViewController.swift:

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. Like the previous method, this fades in the location pin and clears the currently selected infoView. 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:
GoogleMaps_Places_04

Using Directions

In the previous section of the tutorial, you learned one way to draw on the map – using the GMSMarker object. In this section, you’ll learn how to draw a line on the map using the GMSPath and the GMSPolyline objects.

A GMSPath is basically an array of CLLocationCoordinate2Ds. You pass the path to a GMSPolyline, which renders the line on the map. Polylines are a great way to present routes on the map, and you’ll be doing just that using the Google Directions API.

Note: In short, the Google Directions API returns directions from point A to point B according to the parameters you pass it. For example, you can request walking, driving, bicycling or transit directions. For more information, visit the Google Directions API documentation.

Time to draw some lines! Add the following method to MapViewController.swift:

func mapView(mapView: GMSMapView!, didTapInfoWindowOfMarker marker: GMSMarker!) {
  // 1
  let googleMarker = mapView.selectedMarker as PlaceMarker
 
  // 2
  dataProvider.fetchDirectionsFrom(mapView.myLocation.coordinate, to: googleMarker.place.coordinate) {optionalRoute in
    if let encodedRoute = optionalRoute {
      // 3
      let path = GMSPath(fromEncodedPath: encodedRoute)
      let line = GMSPolyline(path: path)
 
      // 4
      line.strokeWidth = 4.0
      line.tappable = true
      line.map = self.mapView
 
       // 5
      mapView.selectedMarker = nil
    }
  }
}

You invoke this method when the user taps the marker’s info view. That’s when you want to display the route.

  1. Cast the given marker to a PlaceMarker.
  2. Call the dataProvider‘s fetchDirections(_to:). This method takes two coordinates and a completion closure that handles an optional encoded route. This special String is an encoded representation of a coordinates array that is decoded when passed to a GMSPath.
  3. Create a GMSPatch from the encoded route and then create a GMSPolyline from the path.
  4. Set the GMSPolyline‘s width, make it tappable and set the map on which it should be displayed.
  5. Clear the map’s selected marker. This removes the currently presented info view.

Build and run your app; tap a marker, then tap its info view and you’ll see a line drawn on the map with the requested route:

GoogleMaps_Directions_01

The blue line color is a bit boring; change it to something more interesting by adding a computed property to MapViewController.swift as follows:

var randomLineColor: UIColor {
  get {
    let randomRed = CGFloat(drand48())
    let randomGreen = CGFloat(drand48())
    let randomBlue = CGFloat(drand48())
    return UIColor(red: randomRed, green: randomGreen, blue: randomBlue, alpha: 1.0)
  }
}

This just returns a random color each time you call the property.

Head back to mapView didTapInfoWindowOfMarker and add the following line of code just after the line where you set the line width:

line.strokeColor = self.randomLineColor

Build and run your app again; this time you’ll get a different line color every time you ask for directions:

GoogleMaps_Directions_02

Where To Go From Here?

Here’s a link to the complete project from this tutorial.

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

  • Unlike MapKit, which updates once a year, the Google Maps SDK updates frequently and has already seen eight major updates since its release. Bugs are being fixed all the time,and with each new release the SDK becomes richer and more stable.
  • When developing cross platform iOS and Android apps, it’s often best to use the same mapping SDK for both platforms. Imagine you have a mapping app that needs to display a walking route on the map. While MapKit has its own solution for this problem, using Google Maps SDK allows you to use the Google Directions API’s encoded route, and receive identical results for both your iOS and Android apps.
  • Google Maps are often more detailed, especially outside the United States. For example, take a look at these two screenshots of central Moscow, one taken from a MapKit map and the other from a Google Maps map:

    Compare_Apple Compare_Google

    Cons

    • MapKit is native to iOS, which means it’s always synced with iOS. For example, Swift works with MapKit out of the box, while using Google Maps requires some adjustments and still lacks full support of the language. Being native also means set up is much easier.
    • The Google Maps SDK still lacks the stability that MapKit has. The SDK crashes more than it should which could scare off users and lead to bad App Store reviews.
    • MapKit has a much better integration with CoreLocation and CoreAnimation. The “Follow user location” mode is missing in Google Maps; as well, the ability to add advanced animations to annotations (MKAnnotation is represented by a UIView, while a GMSMarker is represented by a UIImage) can give your app an appealing touch that the Google Maps SDK just can’t.

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

    For example, you could add a Street View for places the user wants to check out. Additionally, you can further explore the Google Places and Google directions APIs and apply a tighter integration of these two features with Feed Me.

    The Google Maps SDK provides many utility methods as part of the GMSGeometryUtils module. One of these methods is the GMSGeometryDistance C function, which returns a distance between two coordinates. Unfortunately, this method is marked as FOUNDATION_EXPORT OVERLOADABLE which is not supported by Swift and generates an error when used. We expect this to be fixed in future releases of the Google Maps SDK, so we’ve used the existing CLLocation methods for the time being.

    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!

    Introduction to Google Maps iOS SDK in Swift is a post from: Ray Wenderlich

    The post Introduction to Google Maps iOS SDK in Swift appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4370

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>