获取GPS坐标
在这一节课,你要在Xcode中创建MyLocations工程,并且使用Core Location框架来定位用户位置的经度和纬度。
当你完成本小节课时,app看起来回是这样的:
我知道这个界面看起来非常简陋,但是之后我们会完善它。而眼下,唯一重要的事情就是你可以获得GPS坐标,并且将其展现在屏幕上。
和往常一样,我们先一点点的实现每一个小功能,最后来给它一个大整容。
打开Xcode,然后创建一个新的project,这次选择Tabbed Application模版。
然后按照下面写的,填写必要项目:
1、Product Name:MyLocations
2、Organization Name:你的名字或者你公司的名字
3、Organization Identifier:你的身份id,可以用自己倒过来的域名
4、Language:Swift
5、Devices:iPhone
复选框Include Unit和Include UI Test,不要选中
6、保存
运行一下,app的初始界面会是这个样子:
这个app在底部有一个分页栏(tab bar),目前包括两个页面:First和Second。
虽然我们只做了一点点的工作,但是这个app现在已经拥有三个视图控制器了。
1、它的根控制器是UITabBarController,并且包含一个分页栏,可以切换不同的界面。
2、First tab的视图控制器
3、Second tab的视图控制器
这两个页面分别拥有自己的视图控制器。在Xcode中,它们默认的名称是FirstViewController和SecondViewController。
故事模版现在是这个样子的:
我已经把它缩小了,这样它们就可以在一个屏幕上展示全了。故事模版虽然非常便利,但是它占据的空间实在太大了。
和以前一样,你首先在故事模版中将设备型号切换为iPhone SE,最后你会调整app,使它适合全部类型的iPhone。
在故事模版底部找到View as(你已经使用过很多次了),将设备型号切换为iPhone SE。
在这一小节,你将先对first tab这个界面进行处理。在这个课程的后半段,你将处理second界面,以及自行添加第三个分页。
首先,我们来个FirstViewController一个更好听的名字。
打开工程导航器,点击FirstViewController两次,不要点太快,太快就是双击了,然后将其重命名为CurrentLocationViewController.swift。
打开CurrentLocationViewController.swift,将class这一行修改为:
class CurrentLocationViewController: UIViewController {
切换到Mian.storyboard并且选择连接到first tab的视图控制器,打开身份检查器(Identity inspector),将Class文本框中的内容由FirstViewController修改为CurrentLocationViewController:
打开工程设置界面,取消选定Landscape Left和Landscape Right复选框,这样app就只支持竖屏方向了。
运行app,确保前面的修改没有问题。
无论何时,我在故事模版中修改了什么东西都链接时,我都会运行一次app,来验证修改的是否正确,这非常有用。因为在这一过程中很容易遗忘一些微小的步骤。
就像你在Checklists中看到的那样,一个位于导航控制器内的视图控制器会有一个Navigation Item对象,可以用来配置导航栏。Tab Bar的工作方式也和这个类似。每个代表tab页的视图控制器都有一个Tab Bar Item对象。
选中“First Screen”中的Tab Bar Item(就是Current Location View Controller)然后打开属性检查器。将Title改变为Tag。见下图:
稍后,你会在Tab Bar Item上放置一个图片(image);它现在还是使用默认图片,就是一个圆圈。
现在你要为first tab设计界面。它拥有两个button和一些label用来展示GPS坐标以及街道信息。为了给你节省点时间,我们来一次性的把outlet都添加上。
打开CurrentLocationViewController.swift,添加以下代码:
@IBOutlet weak var messageLabel: UILabel!
@IBOutlet weak var latitudeLabel: UILabel!
@IBOutlet weak var longitudeLabel: UILabel!
@IBOutlet weak var addressLabel: UILabel!
@IBOutlet weak var tagButton: UIButton!
@IBOutlet weak var getButton: UIButton!
@IBAction func getLocation() {
// 暂时什么都不做
}
将UI设计为下面这个样子:
Message label位于顶部,并且其宽度应该贴到屏幕的两边。你将使用这个标签来获取app请求GPS坐标时的状态信息。选中这个标签,打开属性检查器,将Alignment设置为cernered,并且把这个标签和messageLabel oulet链接起来。
将latitude goes here和longitude goes here标签右对齐,并且分别和latitudeLabel以及longitude outlet链接起来。
address goes here标签的宽带也需要延展至贴近屏幕两边,并且设置为50 points高,这样就可以容纳两行文本。将其属性检查器中的Lines选项设置为0,这样它就可以容纳多行了。最后将其和addressLabel outlet链接起来。
Tag Location按钮目前还不做任何工作,我们先把它和tabButton outlet链接起来。
将Get My Location按钮和getButton链接起来,并且在链接检查器中设置它的Touch Up Inside事件和getLocation链接起来。
运行一下app,确保一切正常。
到目前为止,没有遇到什么特殊的东西。除了tab bar可能你以前没见过。是时候引入一些新的东西了,让我们来了解一下Core Location!
⚠️:因为最初你是基于iPhone SE的大小来设计界面的,所以你最好用在模拟器中也使用iPhone SE来运行app。如果你在模拟器中用了其他型号的iPhone,可能界面看起来会面目全非,和以前一样,我们会在课程的最后来解决这个问题。
Core Location
绝大多数的iOS设备都可以使你获得自己在这个星球上的位置,无论是通过GPS还是Wifi,亦或者基站的三角测量。而这一能力正是Core Location赋予你的。
任何一个app都可以通过Core Location获得用户目前所在的经度及纬度。对于有指南针的设备,还可以给你提供方向(我们的课程中不会涉及这块内容)。Core Location还可以在你移动时,持续的更新坐标,导航类app就是这个原理。
从Core Location中获取位置信息非常容易,但是你需要躲开一些陷阱。我们开始的时候会简单一些,仅仅是请求当前的坐标,看看会发生些什么。
打开CurrentLocationViewController.swift,在import UIKit下面添加上一行:
import CoreLocation
这样你就把Core Location框架添加到app中了,还有比这更简单的吗?
Core Location和iOS SDK中的大部分一样,通过委托工作,所以你应该让这个视图控制器遵循CLLocationManagerDelegate协议。
改变一下class声明的这一行:
class CurrentLocationViewController: UIViewController,CLLocationManagerDelegate {
这些都放在一行里面。
并且添加一个新的属性:
let locationManager = CLLocationManager()
CLLocationManage就是你用来获取GPS坐标的对象。你将这个对象的引用放入了一个常量中,一旦你创建了location manager对象,那么locationManager的值就不会改变了,它始终指向location manager对象。
这个新的CLLocationManager对象并不会马上给出GPS坐标。为了接收坐标,你需要先调用它的startUpdatingLocation()方法。
除非你是在做导航类app,否则你不需要你的app持续获得用户的位置信息。这样耗电量会非常大。对于我们这个app,你只需要打开location manager一下,在获取到可用的位置信息后,在立马把它关掉。
这会比你想象中复杂一些,目前,我们关心的还是如何接收GPS坐标信息。
将getLocation()方法修改一下:
@IBAction func getLocation() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.startUpdatingLocation()
}
这个方法是链接到Get My Location按钮上的。它告诉location manager这个视图控制器就是它的委托,并且你想要接收精度为10米的位置信息。然后你启动了location manager。在这一刻,CLLocationManager对象会发送位置更新到它的委托,比如这个视图控制器。
和委托交流,需要添加以下代码:
//MARK: - CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("didFailWithError \(error)")
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let newLocation = locations.last!
print("didUpdateLocations \(newLocation)")
}
它们是用于location manager的委托方法。暂时,你只是简单的在调试区域打印一些信息,来看看location manager是否工作。
//MARK
在刚才那段代码的第一行,是一行以//MARK:开头的注释,像这样的注释会在Xcode中将你的源代码进行巧妙的分节,你可以在Jump Bar中看到它们:
这里的“-”符号作用是让Xcode做出分割线,就是所谓的华丽的分割线。你也可以添加一些类似于//TODO:或者//FIXME:之类的东西,它们也会在Jump Bar中出现,其作用相当于是一篇文章的小目录,使你的代码更加清晰可读。
在模拟器中运行app,并且点击Get My Location按钮,和作者以往的套路一样,什么都不会发生。并且作者会很贴心的告诉你,这是因为你没有获取到用户的许可的缘故。
在getLocation()方法的顶部添加以下代码:
let authStatus = CLLocationManager.authorizationStatus()
if authStatus == .notDetermined {
locationManager.requestWhenInUseAuthorization()
return
}
这些代码的作用是检查目前的许可状态,如果是禁止访问位置信息的话,app就会在用户使用时(When In Use),进行许可请求。
还有一种请求方法是“Always”,这种授权可以使app在未激活时也会检查用户的位置信息。其实这两个种方式就是大家常见的,使用时允许和总是允许。总是允许对导航类app是非常重要的,但是对于我们的app,仅仅在用户使用时允许就够了。
仅仅是添加这些代码是不够的,你要需要在Info.plist中添加一个特殊的键值。
打开Info.plist,然后使用鼠标右键打开菜单,选择Add Row:
新增一行的时候,在键的位置输入:NSLocationWhenInUseUsageDescription或者从下拉框中选择:Privacy - Location When In Use Usage Description。
然后在值的部分,输入文本:This app lets you keep track of interesting places. It needs access to the
GPS coordinates for your location.
这里的文本描述了你为什么要获取用户的位置信息。
运行app,再次点击Get My Location按钮。
Core Location此时会弹出一个窗口,向用户请求获取位置信息的许可:
如果用户拒绝了这个请求,那么Core Location就无法获取到用户的位置信息。
点击Don't Allow,然后再点击Get My Location按钮。
Xcode的debug区域会显示出一条消息:
didFailWithError Error Domain=kCLErrorDomain Code=1
这条消息来自于locationManager(didFailWithError)委托方法。它告诉你location manager无法获取位置信息。
为什么无法获取的原因由一个Error对象描述,它是iOS SDK中的标准对象,用于传达错误信息。你会在SDK中的许多地方见到它(因为有太多的地方容易出错了)
Error对象有一个“domain(域)”和“code(代码)”。在这里domain是kCLErrorDomain,意思是这个错误是来自Core Location(CL)。code为1,代表CLError.denied,意思是用户没有授权这个app可以获得位置信息。
⚠️:k前缀经常被iOS框架用来表示某个名称是常量,我猜这个k是konstant的首字母。这常量的一种旧式用法,你不会经常看到它了,但是它仍旧会在一些角落里出现,比如这里。
在Xcode中停掉app,然后再次运行。
当你点击Get My Location按钮时,app不会再次向用户请求许可,你会在debug区域看到一条和之前一模一样的报错信息。
让我们来改进一下它的用户体验,使它更加友好一些,因为用户才看不到什么报错信息。
打开CurrentLocationViewController.swift,添加以下方法进去:
func showLocationServicesDeniedAlert() {
let alert = UIAlertController(title: "Location Services Disabled", message: "Please enable location services for this app in Settings.", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
present(alert,animated: true,completion: nil)
}
这个弹出窗口会展示一条很有用的信息。如果无法获取位置信息的话,这个app就是无用的,所以应该鼓励用户打开location服务的授权。
在getLocation()方法中调用这个新的方法,将下面的代码放在设置locationManager的委托之前:
if authStatus == .denied || authStatus == .restricted {
showLocationServicesDeniedAlert()
return
}
如果authorization状态为denied或者restricted,就会弹出这个窗口,提醒用户去设置中授权该app可以使用位置信息。注意这里的||符号,是逻辑或的意思。当两个条件之一为true时,showLocationServicesDeniedAlert()方法就会被调用。
再次运行app,点击Get My Location按钮,你现在应该看到一个弹窗,提示你在设置中开启允许获得位置信息。
当用户改变主意,想要允许这个app使用位置信息时,就可以在设置中开启这一功能。
打开模拟器中app的Setting,然后在Privacy->Location中打开允许获得位置信息,就是私隐->位置信息菜单:
选择MyLocations,然后选择While Using the App(使用时允许),来开启location服务。回到app,然后再点击Get My Location按钮。
当我这样做时,我又再Xcode的dubug区域看到一条报错信息:
didFailWithError Error Domain=kCLErrorDomain Code=0
这一次有点不一样,code为0。这是“location unknown”的意思,就是说Core Location由于某些原因无法获取位置信息。
不用对此太惊讶,当你在模拟器中运行app时,显然你并不具备一个真正的GPS模块。你的Mac有这个功能,但是无法分享给模拟器中的app,幸运的事,我们还是有办法来测试这个功能。
保持app运行,然后在模拟器的菜单中选择Debug->Location->Apple。
此时你应该可以在debug区域看到类似下面的信息:
didUpdateLocations <+37.33259552,-122.03031802> +/- 500.00m (speed -1.00
mps / course -1.00) @ 7/19/16 4:03:52 PM Central European Summer Time
didUpdateLocations <+37.33241023,-122.03051088> +/- 65.00m (speed -1.00
mps / course -1.00) @ 7/19/16 4:03:54 PM Central European Summer Time
didUpdateLocations <+37.33233141,-122.03121860> +/- 50.00m (speed -1.00
mps / course -1.00) @ 7/19/16 4:04:01 PM Central European Summer Time
didUpdateLocations <+37.33233141,-122.03121860> +/- 30.00m (speed 0.00
mps / course -1.00) @ 7/19/16 4:04:03 PM Central European Summer Time
didUpdateLocations <+37.33233141,-122.03121860> +/- 10.00m (speed 0.00
mps / course -1.00) @ 7/19/16 4:04:05 PM Central European Summer Time
debug区域中的消息在不断的刷新,几乎每秒就更新一次位置信息,虽然经纬度没有发生变化。这些坐标的位置,其实是苹果的总部,加利佛尼亚,库比蒂诺。
仔细看看获取到的坐标信息,最初的是"+- 500.00m",中间阶段是"+/- 65.00m"和"+/- 50.00m",最终保持在"+/- 5.00m"。
这些信息代表测量的精度,单位是米。模拟器真实的模拟着真实设备上的情况,获取位置时,精度不断的缩小范围。
真实的iPhone设备有三种获取位置信息的方式,基站三角测量,Wifi和GPS,其中:
基站三角测量只要手机有信号就工作,但是精度不是非常高。
Wifi会稍微好一些,但是仅仅在你周围有Wifi时才能工作。原理是使用一个包含无线网络设备位置信息的一个大数据库。
GPS的测量精度是最高的,但是由于它是和卫星通讯,所以是三种方法中最慢的,并且时常在室内会失灵。
现在你知道了设备有三种方式获取位置信息,并且按照速度排序的话,基站三角测量和Wifi比较快,GPS最慢。并且这三种方法都不能保证随时正常工作。有些设备甚至没有GPS模块和通信模块,只能依靠Wifi获取位置信息。获取位置信息这件事一下子就变得看起来很棘手了。
幸运的是,Core Location把这些多种渠道读取位置信息并且转换为数字的复杂工作自己做完了。Core Location不会一直等待从GPS获取数据,而是把能优先获取到的数据先展示出来,然后再慢慢的提高精度。
练习:如果你手边有iPhone或者iPod、iPad,在这些设备上都试试你的app,看看都能得到什么样的数据,注意一下它们的不同之处。