地图和定位相关研究(一) —— Google Maps的集成(一)

版本记录

版本号 时间
V1.0 2020.03.14 星期六

前言

定位和地图是很多App中必有的功能,这里单独抽出来模块一起学习和探讨。感兴趣的可以多指正,大家一起进步。对于苹果地图框架MapKit我已经单独列出来了 - MapKit框架详细解析 - 会随时更新。感兴趣的可以看下。这里只说下三方地图SDK的相关集成和其他相关问题。

开始

差不多一个多月没更新了,是状态有点差,在慢慢调节,谢谢大家继续支持!

首先看下主要内容

您将学习如何使用Google Maps iOS SDK制作应用程序,以搜索附近的饮食场所或杂货店。

下面看下写作环境

Swift 5, iOS 13, Xcode 11

在2012年之前,Google Maps是所有iOS设备的地图引擎。 那年,苹果公司通过内部地图引擎MapKit取代了Google Maps,从而在iOS 6中做出了巨大的改变。

几个月后,Google发布了自己的iOS独立Google Maps应用程序,以及面向开发人员的Google Maps iOS SDK

MapKitGoogle Maps iOS SDK都有各自的优缺点。 您必须决定哪一种最适合您的用例。 如果您决定使用Google Maps SDK,则本教程适合您!

在本教程中,您将构建一个名为Feed Me的应用程序,该应用程序将获取用户的当前位置,并搜索附近的饮食场所或杂货店。 在此过程中,您将学习如何使用Google Maps iOS SDK

  • 获取用户的位置。
  • 显示街道地址。
  • 显示附近的搜索结果。
  • 提供找到的地方的额外相关信息。

打开已有工程Feed Me.xcworkspace

熟悉一下该项目。 需要注意的重要事项是:

  • MapViewController.swift:此项目的主视图控制器,也是您将在本教程中修改的唯一视图控制器。
  • GoogleDataProvider.swift:包装器类,用于进行Google API调用。 您将在本教程的后面部分中回顾其中包含的方法。
  • GooglePlace.swift:为Google返回的地方结果建模。
  • MarkerInfoView.swiftUIView的子类,用于显示位置的详细信息。 它带有一个匹配的.xib文件。

在开始编码之前,请构建并运行您的应用程序。 您会看到以下屏幕:

现在,您会看到的是一个空白屏幕,中间有一个大头针。 接下来,按导航栏右侧的操作按钮以查看TypesTableViewController屏幕:

目前,所有这些都可以在应用中看到。 您可以添加一些魔法!


Creating the API Key

您需要做的第一件事是使用Google Maps SDKGoogle API的API密钥。 如果您还没有Google帐户,请创建一个(免费!),然后登录Google Developers Console。

单击Create以创建一个新项目,将您的项目命名为Feed Me,然后单击Create

如果您的项目没有立即显示,请刷新页面直到显示出来。 在新创建的项目中,选择Enable APIs and services

搜索并启用以下API:

  • Maps SDK for iOS
  • Places API

在左侧菜单面板的APIs & Services下,选择Credentials。 单击Create Credentials,然后单击API key以创建密钥:

您必须先添加实际的Google Maps iOS SDK,然后才能使用该密钥。 因此,暂时保持窗口打开。


Adding the SDK

Pods项目中打开Podfile,然后在结尾上方添加以下内容:

pod 'GoogleMaps', '~> 3.7.0'

接下来,打开终端并使用cd命令导航到包含Feed Me项目的目录

cd ~/Path/To/Folder/Containing/Feed Me

输入以下命令以安装Google Maps iOS SDK

pod install

您应该看到如下输出:

Analyzing dependencies
Downloading dependencies
Installing GoogleMaps 3.7.0
Generating Pods project
Integrating client project

现在,您的项目中已有GoogleMapsCocoaPods使生活更加轻松!

打开AppDelegate.swift并将其内容替换为以下内容:

import UIKit
import GoogleMaps

//1
let googleApiKey = "ENTER_KEY_HERE"

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?

  func application(
    _ application: UIApplication, 
    didFinishLaunchingWithOptions launchOptions: 
      [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    //2
    GMSServices.provideAPIKey(googleApiKey)
    return true
  }
}

这里有两个新元素:

  • 1) 用来保存您的Google API密钥的常量。 将ENTER_KEY_HERE替换为您先前创建的Google API密钥。
  • 2) 使用GMSServices类方法ProvideAPIKey()使用API密钥实例化Google Maps服务。

Creating the Map View

现在您已经有了API密钥,您可以注销并关闭Google Developers Console窗口。

1. Adding a UIView

首先打开Main.storyboard来启动Interface Builder。 找到MapViewController场景,然后将简单的UIView从对象库中拖到MapViewController视图的大致中心。 使用View ▸ Show Library以显示对象库。

将视图的背景颜色更改为浅灰色。 接下来,使用Editor ▸ Outline打开Document Outline,然后重新排列视图层次结构,以便对象树如下所示:

要将这个简单的UIView变成GMSMapView,请选择刚刚添加的视图,然后通过从Utilities工具栏中选择第三个选项卡来打开Identity inspector。 将视图的Class更改为GMSMapView,如以下屏幕截图所示:

你的MapViewController应该如下所示:

2. Adding the Constraints for the Map View

接下来,您将添加一些约束以使地图覆盖整个屏幕。

Document Outline中选择Map View,然后选择Interface Builder窗口右下角的中间按钮。 这是Pin按钮。

确保未选中Constrain to margins(确保地图将填充屏幕上的所有可用空间),并从父视图的顶部,左侧,底部和右侧添加0个空间约束。

您的Pin编辑器应如下所示:

单击Add 4 Constraints以将约束添加到地图视图。
您的MapViewController场景应如下所示,其中灰色区域代表GMSMapView

3. Creating an Outlet for the Map View

在构建和运行项目之前,请为地图视图添加IBOutlet。 为此,请使用键盘快捷键Command-Option-Control-Return调出助手编辑器。

Interface Builder中选择地图视图,按住Control键,然后将一条线从地图视图拖到MapViewController.swift。 将会出现一个弹出窗口。 将连接类型设置为Outlet,将“名称”设置为mapView。 将类型保留为GMSMapView并单击Connect

这将在MapViewController.swift中创建一个GMSMapView属性,并在Interface Builder中自动将其连接。 最后,在import UIKit之后,将以下内容添加到文件顶部:

import GoogleMaps

构建你将会看见一个地图

您现在正在应用中使用Google Maps iOS SDK。 但是,除了显示基本地图之外,您还可以做其他事情,对吧?下一步将是根据您的用户所在位置自定义地图。


Getting the Location

Feed Me就是寻找用户附近的地方,而如果不获取用户的位置,您将无法做到这一点。

1. Adding MapViewController Extension

iOS使用代理通知用户位置信息,你的下一步就是MapViewController遵循CLLocationManagerDelegate

// MARK: - CLLocationManagerDelegate
//1
extension MapViewController: CLLocationManagerDelegate {
  // 2
  func locationManager(
    _ manager: CLLocationManager,
    didChangeAuthorization status: CLAuthorizationStatus
  ) {
    // 3
    guard status == .authorizedWhenInUse else {
      return
    }
    // 4
    locationManager.requestLocation()

    //5
    mapView.isMyLocationEnabled = true
    mapView.settings.myLocationButton = true
  }

  // 6
  func locationManager(
    _ manager: CLLocationManager,
    didUpdateLocations locations: [CLLocation]) {
    guard let location = locations.first else {
      return
    }

    // 7
    mapView.camera = GMSCameraPosition(
      target: location.coordinate,
      zoom: 15,
      bearing: 0,
      viewingAngle: 0)
  }

  // 8
  func locationManager(
    _ manager: CLLocationManager,
    didFailWithError error: Error
  ) {
    print(error)
  }
}

在此扩展程序中,您:

  • 1) 创建一个符合CLLocationManagerDelegateMapViewController扩展。
  • 2) 创建一个接受CLAuthorizationStatus参数的locationManager委托方法。 用户授予或撤销位置权限后,将调用此方法。
  • 3) 验证用户在使用该应用程序时是否已授予您权限。
  • 4) 询问位置经理他们的位置。
  • 5) 添加用户的位置指示器和位置按钮。
  • 6) 创建另一个接受CLLocation数组的locationManager方法。 这在位置管理器接收到新的位置数据时执行。
  • 7) 将地图的摄像头更新到用户当前位置附近的中心。 GMSCameraPosition类汇总所有摄像机位置参数,并将它们传递给地图以进行显示。
  • 8) 创建一个接受Error参数的第三个locationManager。 如果应用程序引发错误,它将打印到控制台,以便您可以处理它并防止应用程序崩溃。

2. Creating an Instance of CLLocationManager

现在,您必须完成要求用户位置的工作。

首先,将以下属性添加到MapViewController

let locationManager = CLLocationManager()

这将添加并实例化名为locationManagerCLLocationManager属性。

接下来,在扩展名中找到viewDidLoad()的定义,并将其替换为以下内容:

override func viewDidLoad() {
  super.viewDidLoad()

  // 1
  locationManager.delegate = self

  // 2
  if CLLocationManager.locationServicesEnabled() {
    // 3
    locationManager.requestLocation()

    // 4
    mapView.isMyLocationEnabled = true
    mapView.settings.myLocationButton = true
  } else {
    // 5
    locationManager.requestWhenInUseAuthorization()
  }
}

这是这样做的:

  • 1) 使MapViewController成为locationManager的委托。
  • 2) 检查用户先前是否已授权使用此应用的位置服务。
  • 3) 向位置管理器询问用户的位置。
  • 4) 将isMyLocationEnabledmyLocationButton设置为true。 这会绘制一个浅蓝色的点来指示用户的位置,并添加一个按钮以将地图居中。
  • 5) 如果locationServicesEnabledfalse,则在使用应用程序时请求访问用户的位置。

您已经准备好检查许可,但仍然需要先申请许可。 接下来,您将对其进行修复。

3. Asking for Permission to Get Location

打开项目导航器顶部的Feed Me项目,然后选择Feed Me目标。 然后转到Info选项卡,然后在Custom iOS Target Properties部分中选择第一行。

按住Control键并单击,然后从菜单中选择Raw Keys & Values。 现在,单击+图标以添加新行。

NSLocationWhenInUseUsageDescription设置为键,选择String作为类型,然后输入以下文本作为值:

By accessing your location, this app can find good places to eat for you.

完成后,它将如下所示:

构建并运行。 应用加载后,系统会提示您一条警告,要求您提供位置权限。 点击Allow While Using App

现在,您会看到以您的位置为中心的地图。 稍微滚动地图,然后点击右下角的Locate按钮。 地图再次以您的位置为中心。


Showing the Street Address

现在您已经有了用户的位置,现在可以显示该位置的街道地址了。 Google Maps有一个对象可以做到这一点:GMSGeocoder。 这需要一个坐标并返回可读的街道地址。

1. Creating the UI for the Address

首先,您将添加一些UI,以向用户显示地址。

打开Main.storyboard并将UILabel添加到MapViewController场景。 确保将UILabel添加到MapViewController的视图中,而不是GMSMapView

接下来,打开Attributes inspector,并为标签设置以下属性:

  • 居中对齐。
  • 行数为0。令人惊讶的是,这使label可以容纳任意多行以适合文本。
  • 背景为白色,不透明度为85%

完成后,标签的Attributes inspector和场景的Object Tree应如下所示:

另外,请确保所有子视图的排序如下:

最后,为标签的左侧,底部和右侧添加0个空间约束,如下所示:

这将标签固定在屏幕的底部,并将其延伸到整个屏幕的宽度。

你的故事板场景应该是这样的:

接下来,为标签创建一个outlet

打开Assistant Editor并将其从Document Outline中的标签拖拽到MapViewController.swift。将连接类型设置为Outlet,将名称设置为addressLabel,然后单击Connect

这增加了一个属性到你的MapViewController,你可以在你的代码中使用:

@IBOutlet weak var addressLabel: UILabel!

2. Getting Address From a Coordinate

现在您已经有了地址label,是时候获取地址了。添加下面的方法到MapViewController:

func reverseGeocode(coordinate: CLLocationCoordinate2D) {
  // 1
  let geocoder = GMSGeocoder()

  // 2
  geocoder.reverseGeocodeCoordinate(coordinate) { response, error in
    guard 
      let address = response?.firstResult(), 
      let lines = address.lines 
      else {
        return
    }

    // 3
    self.addressLabel.text = lines.joined(separator: "\n")

    // 4
    UIView.animate(withDuration: 0.25) {
      self.view.layoutIfNeeded()
    }
  }
}

下面是每个注释部分的功能:

  • 1) 创建GMSGeocoder对象,将纬度和经度坐标转换为街道地址。
  • 2) 请求地理编码器对传递给方法的坐标进行反向地理编码。然后验证GMSAddress类型的响应中是否有地址。这是GMSGeocoder返回地址的模型类。
  • 3) 将addressLabel的文本设置为地理编码器返回的地址。
  • 4) 一旦地址被设置,在标签的intrinsic content size的变化中产生动画。

下一步是在用户更改位置时保持地图更新。

3. Updating the Address

每当用户更改地图上的位置时,您都希望调用上面的方法。为此,您将使用GMSMapViewDelegate

添加另一个扩展到底部的MapViewController.swift如下:

// MARK: - GMSMapViewDelegate
extension MapViewController: GMSMapViewDelegate {
}

这将声明MapViewController符合GMSMapViewDelegate

接下来,将以下代码添加到viewDidLoad():

mapView.delegate = self

这使得MapViewController成为地图视图的委托。

最后,将以下方法添加到新添加的扩展名中:

func mapView(_ mapView: GMSMapView, idleAt position: GMSCameraPosition) {
  reverseGeocode(coordinate: position.target)
}

每当地图停止移动并定位到新位置时,将调用此方法。然后调用反地理编码到新位置并更新addressLabel的文本。

构建和运行。您将在屏幕底部看到当前位置的地址—真实的或模拟的。

注意到这幅图有什么问题吗?

谷歌logo和定位按钮隐藏在标签后面!

4. Fixing the Street Address UI

幸运的是,GMSMapView为此提供了一个非常简单的解决方案:填充padding。当您对地图应用内边距时,所有的视觉元素都将根据该内边距放置。

返回到reverseGeocodeCoordinate(_:)并在动画块之前添加这些行:

// 1
let labelHeight = self.addressLabel.intrinsicContentSize.height
let topInset = self.view.safeAreaInsets.top
self.mapView.padding = UIEdgeInsets(
  top: topInset,
  left: 0,
  bottom: labelHeight,
  right: 0)

现在,将动画块替换为:

UIView.animate(withDuration: 0.25) {
  //2
  self.pinImageVerticalConstraint.constant = (labelHeight - topInset) * 0.5
  self.view.layoutIfNeeded()
}

这做了两件事:

  • 1) 在动画块之前向地图的顶部和底部添加填充。顶部的内边距等于视图顶部的安全区域间距,而底部的内边距等于标签的高度。
  • 2) 通过调整地图的垂直布局约束来更新定位大头针的位置以匹配地图的填充。

构建并再次运行。这一次,一旦标签变得可见,谷歌logolocate按钮将移动到它们的新位置。

移动地图,您将注意到,每当地图定位到新位置时,地址都会发生变化。你将添加动画来减弱这种效果。

将以下方法添加到GMSMapViewDelegate扩展中:

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

每当地图开始移动时,都会调用此方法。它接收一个Bool,该Bool告诉您移动是源于用户手势(如滚动地图),还是源于代码。

如果移动来自用户,您可以调用addressLabel上的lock()来给它一个加载动画。

当有一个lock()时,也必须有一个相应的unlock()。添加以下内容作为传递给reverseGeocode(coordinate:)的闭包的第一行。它应该正好在guard let之前:

self.addressLabel.unlock()

注意:要获得lock()unlock()的完整实现,请查看UIView+Extensions.swift

构建和运行。当您滚动地图时,您将在地址标签上看到一个加载动画。


Finding Places

现在您已经设置好了地图,并掌握了用户的位置,是时候为该用户提供内容了!

您将使用谷歌Google Places API来搜索用户所在位置附近的餐饮场所。这是一个免费的web服务API,您可以使用它来查询任何给定点附近的场所、地理位置或其他感兴趣的点。

1. Marking Locations on the Map

谷歌Maps iOS SDK为您提供了GMSMarker类来在地图上标记位置。每个标记对象包含一个坐标和一个图标图像,并在添加它时在地图上呈现。

对于这个应用程序,您将提供每个标记的附加信息。为此,您将创建GMSMarker的一个子类。

创建一个新的Cocoa Touch类,命名为PlaceMarker,并使它成为GMSMarker的子类。确保您选择了Swift作为该文件的语言。

PlaceMarker.swift的内容替换为:

import UIKit
import GoogleMaps

class PlaceMarker: GMSMarker {
  // 1
  let place: GooglePlace

  // 2
  init(place: GooglePlace, availableTypes: [String]) {
    self.place = place
    super.init()

    position = place.coordinate
    groundAnchor = CGPoint(x: 0.5, y: 1)
    appearAnimation = .pop

    var foundType = "restaurant"
    let possibleTypes = availableTypes.count > 0 ? 
      availableTypes : 
      ["bakery", "bar", "cafe", "grocery_or_supermarket", "restaurant"]

    for type in place.types {
      if possibleTypes.contains(type) {
        foundType = type
        break
      }
    }
    icon = UIImage(named: foundType+"_pin")
  }
}

下面是这段代码的作用:

  • 1) 将GooglePlace类型的属性添加到PlaceMarker
  • 2) 声明一个新的指定初始化器,它接受GooglePlace和可用的位置类型。完全初始化一个PlaceMarker与位置,图标图像和锚标记的位置以及外观动画。

2. Searching for Nearby Places

接下来,添加两个属性到MapViewController:

let dataProvider = GoogleDataProvider()
let searchRadius: Double = 1000

你将使用在GoogleDataProvider.swift中定义的dataProvider。来调用谷歌Places Web API。您将使用searchRadius来设置与用户的搜索距离。你把它设为1000米。

添加以下方法到MapViewController:

func fetchPlaces(near coordinate: CLLocationCoordinate2D){
  // 1
  mapView.clear()
  // 2
  dataProvider.fetchPlaces(
    near: coordinate,
    radius:searchRadius,
    types: searchedTypes
  ) { places in
    places.forEach { place in
      // 3
      let marker = PlaceMarker(place: place, availableTypes: self.searchedTypes)
      // 4
      marker.map = self.mapView
    }
  }
}

在这个方法中,你:

  • 1) 清除地图上所有的标记。
  • 2) 使用dataProvidersearchRadius内查询谷歌,根据用户选择的类型进行筛选。
  • 3) 迭代完成闭包中返回的结果,并为每个结果创建一个PlaceMarker
  • 4) 设置标记的地图。这行代码告诉地图呈现标记。

这里有一个价值64,000美元的问题:什么时候调用这个方法?

答案是:当应用程序启动时。当你的用户打开一个名为Feed Me的应用程序时,他们将会看到一些吃饭的地方!

找到locationManager(_:didUpdateLocations:)并在末尾添加以下代码行:

fetchPlaces(near: location.coordinate)

接下来,由于用户可以更改要在地图上显示的位置类型,所以如果所选类型发生更改,您将更新搜索结果。

为此,找到typesController(_:didSelectTypes:)并在末尾添加以下代码行:

fetchPlaces(near: mapView.camera.target)

最后,当用户的位置发生变化时,您将为用户提供获取新位置的选项。

3. Adding a Refresh Map Option

打开Main.storyboar。将一个UIBarButtonItem从对象库拖到MapViewController导航栏的左侧。将按钮的System Item更改为Refresh,如下所示:

打开助理编辑器并从刷新按钮拖拽到MapViewController.swift。选择Action并将该方法命名为refreshPlaces。在新添加的方法中插入如下代码:

fetchPlaces(near: mapView.camera.target)

构建和运行,你会看到位置大头针弹出周围的地图。改变TypesTableViewController中的搜索类型,看看结果是如何变化的。

注意:如果您在更改搜索类型后没有看到任何新的标记,这可能是由于谷歌的使用限制。等几秒钟,然后尝试刷新。有关更多信息,请查看官方的谷歌Places使用限制文档 - official Google Places usage limits documentation。

这些标记确实为地图增加了一些颜色,但是如果没有额外的数据来提供用户关于固定位置的详细信息,这些标记就没有多大用处。

4. Showing Additional Place Information

将以下方法添加到MapViewController.swift中的GMSMapViewDelegate扩展中:

func mapView(
  _ mapView: GMSMapView, 
  markerInfoContents marker: GMSMarker
) -> UIView? {
  // 1
  guard let placeMarker = marker as? PlaceMarker else {
    return nil
  }

  // 2
  guard let infoView = UIView.viewFromNibName("MarkerInfoView") as? MarkerInfoView 
    else {
      return nil
  }

  // 3
  infoView.nameLabel.text = placeMarker.place.name
  infoView.addressLabel.text = placeMarker.place.address

  return infoView
}

每次点击一个标记都会调用这个方法。如果您返回一个视图,它将出现在标记的上方。如果返回nil,按钮点击什么也不做。

你要做的是:

  • 1) 您首先将点击标记投射到PlaceMarker上。
  • 2)接下来,从它的nib创建一个MarkerInfoViewMarkerInfoView是本教程的入门项目附带的一个UIView子类。
  • 3)然后将地名应用到nameLabel,将地址应用到addressLabel

一切都运行得很好,但是在完成应用程序之前,您还有一个步骤来确保UI看起来是正确的。

5. Tidying up the UI

您需要确保位置大头针不覆盖信息窗口。为此,将以下方法添加到GMSMapViewDelegate扩展中:

func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool {
  mapCenterPinImage.fadeOut(0.25)
  return false
}

当用户点击一个标记时,这个方法隐藏定位针。该方法返回false,以指示您不希望在单击标记时覆盖将地图居中的默认行为。

但是大头针需要在某个时候重新出现。因此,将以下内容添加到mapView(_:willMove:)的末尾:

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

这将检查动作是否源于用户手势。如果是这样,则使用fadeIn(_:)来断开位置pin。它还将地图的selectedMarker设置为nil,以删除当前呈现的infoView

最后,将以下方法添加到GMSMapViewDelegate扩展中:

func didTapMyLocationButton(for mapView: GMSMapView) -> Bool {
  mapCenterPinImage.fadeIn(0.25)
  mapView.selectedMarker = nil
  return false
}

当用户点击Locate按钮时,此方法将运行,从而使地图以用户的位置为中心。它还返回false,这样就不会覆盖按钮的默认行为。

构建和运行。选择一个标记,你会看到位置淡出。滚动地图,注意到infoView关闭并把大头针带回来:

就这样,你做到了!你现在有一个功能齐全的谷歌地图应用程序。


Google Maps Versus Apple MapKit

当您考虑构建自己的基于地图的应用程序时,您可能想知道是否应该使用MapKit。下面是每个SDK的一些优点,可以帮助您决定在您的情况下使用哪一个:

1. Advantages of Google Maps iOS SDK

  • SDK的频繁更新。
  • 跨平台(iOS和Android)应用的统一体验。
  • MapKit更详细的地图,特别是在美国以外的地方。

2. Advantages of Apple’s MapKit

  • 原生iOS。MapKit总是与iOS同步,并与开箱即用的Swift协同工作。
  • 更大的稳定性。
  • 更好地与CoreLocationCoreAnimation集成。

谷歌Google Maps SDK太大了,不能包含在这个下载中,所以在运行构建之前一定要运行pod install

本教程仅向您展示了谷歌Maps SDK的基本功能。还有很多东西要学;您一定要查看这个SDK提供的其他优秀特性的完整文档full documentation。

例如,谷歌Maps iOS SDK还可以显示方向、室内地图、覆盖层、平铺层和街景。为了获得额外的权限,尝试使用这些功能来增强Feed Me应用程序。

后记

本篇主要讲述了Google Maps的集成,感兴趣的给个赞或者关注~~~

你可能感兴趣的:(地图和定位相关研究(一) —— Google Maps的集成(一))