版本记录
版本号 | 时间 |
---|---|
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
。
MapKit
和Google 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.swift:
UIView
的子类,用于显示位置的详细信息。 它带有一个匹配的.xib
文件。
在开始编码之前,请构建并运行您的应用程序。 您会看到以下屏幕:
现在,您会看到的是一个空白屏幕,中间有一个大头针。 接下来,按导航栏右侧的操作按钮以查看TypesTableViewController
屏幕:
目前,所有这些都可以在应用中看到。 您可以添加一些魔法!
Creating the API Key
您需要做的第一件事是使用Google Maps SDK
和Google 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
现在,您的项目中已有GoogleMaps
。 CocoaPods
使生活更加轻松!
打开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) 使用
GMSService
s类方法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) 创建一个符合
CLLocationManagerDelegate
的MapViewController
扩展。 - 2) 创建一个接受
CLAuthorizationStatus
参数的locationManager
委托方法。 用户授予或撤销位置权限后,将调用此方法。 - 3) 验证用户在使用该应用程序时是否已授予您权限。
- 4) 询问位置经理他们的位置。
- 5) 添加用户的位置指示器和位置按钮。
- 6) 创建另一个接受
CLLocation
数组的locationManager
方法。 这在位置管理器接收到新的位置数据时执行。 - 7) 将地图的摄像头更新到用户当前位置附近的中心。
GMSCameraPosition
类汇总所有摄像机位置参数,并将它们传递给地图以进行显示。 - 8) 创建一个接受
Error
参数的第三个locationManager
。 如果应用程序引发错误,它将打印到控制台,以便您可以处理它并防止应用程序崩溃。
2. Creating an Instance of CLLocationManager
现在,您必须完成要求用户位置的工作。
首先,将以下属性添加到MapViewController
:
let locationManager = CLLocationManager()
这将添加并实例化名为locationManager
的CLLocationManager
属性。
接下来,在扩展名中找到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) 将
isMyLocationEnabled
和myLocationButton
设置为true
。 这会绘制一个浅蓝色的点来指示用户的位置,并添加一个按钮以将地图居中。 - 5) 如果
locationServicesEnabled
为false
,则在使用应用程序时请求访问用户的位置。
您已经准备好检查许可,但仍然需要先申请许可。 接下来,您将对其进行修复。
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) 通过调整地图的垂直布局约束来更新定位大头针的位置以匹配地图的填充。
构建并再次运行。这一次,一旦标签变得可见,谷歌logo
和locate
按钮将移动到它们的新位置。
移动地图,您将注意到,每当地图定位到新位置时,地址都会发生变化。你将添加动画来减弱这种效果。
将以下方法添加到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) 使用
dataProvider
在searchRadius
内查询谷歌,根据用户选择的类型进行筛选。 - 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
创建一个MarkerInfoView
。MarkerInfoView
是本教程的入门项目附带的一个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
协同工作。 - 更大的稳定性。
- 更好地与
CoreLocation
和CoreAnimation
集成。
谷歌Google Maps SDK
太大了,不能包含在这个下载中,所以在运行构建之前一定要运行pod install
。
本教程仅向您展示了谷歌Maps SDK
的基本功能。还有很多东西要学;您一定要查看这个SDK提供的其他优秀特性的完整文档full documentation。
例如,谷歌Maps iOS SDK
还可以显示方向、室内地图、覆盖层、平铺层和街景。为了获得额外的权限,尝试使用这些功能来增强Feed Me
应用程序。
后记
本篇主要讲述了Google Maps的集成,感兴趣的给个赞或者关注~~~