Core Location in iOS 8Mike Lazer-Walker April Peng 🚩🌱

自从 iPhone 存在以来,位置服务就一直处于非常重要的位置。Maps.app 是第一代 iPhone 里杀手锏的功能之一。Core Location API 也在 iPhone OS SDK 最初的公开版本里就存在了。每一次发布 iOS,Apple 都会给这个库逐步添加新功能,比如后台运行的位置服务,坐标化,以及室内定位( iBeacons )。

iOS 8 仍然继续坚定的延续着这个进程。跟其他最新的更新类似,Core Location 被改动了不少,不管是允许开发者做之前并不被允许的开发,还是帮助维护用户隐私。更特别的是,iOS 8 给 Core Location 带来了三个主要的改进:更分化的权限,室内定位以及访问监控。

权限

一个 app 总有各种各样的理由需要得到你的位置信息。一个能够提示你每个转弯在哪里的 GPS 应用就需要持续获得你的位置信息,才可以在转弯的时候提示你。一个餐厅推荐的 app 也需要得到你的位置信息(即便它并没有打开的情况下),才可以在你到你朋友点赞的餐厅附近的时候能收到推送消息。一个 Twitter 应用在发推的时候也可能需要你的位置,但在你不使用的时候不应该监控你的位置。

在 iOS 8 之前,位置服务的权限是二元的:你要么赋予一个应用得到使用位置服务的权限,要么不给。你可以在 Settings.app 查看哪些 app 可以在后台取得你的位置信息,但除了完全不让这个 app 使用位置服务之外,你不能做任何的事来阻止它获取位置信息。

iOS 8 修改了这个问题,它把位置服务权限拆分成了 2 个不同的授权。

这是对用户隐私的一个重大改进,但对于我们开发者来说则意味着多一些的工作。

取得权限

在早前的 iOS 版本中,获取位置服务权限是隐式的。比如 CLLocationManager,如果应用程序还没有被许可或者之前被拒绝了的话,下面的代码会触发系统弹出提示框向用户获取位置服务的授权:

import Foundation
import CoreLocation

let manager = CLLocationManager()
if CLLocationManager.locationServicesEnabled() {
    manager.startUpdatingLocation()
}

把事情简化一下,假定我们声明了一个 manager 实例作为所有例子的成员变量,它的 delegate 是它的 owner。

CLLocationManager 取得最新的位置的这个操作会让系统弹出是否允许位置服务的提示。

在 iOS 8,取得权限和使用位置服务已经分成两个动作了。分别用两个不同的方法取得权限:requestWhenInUseAuthorizationrequestAlwaysAuthorization。前者只能让应用在使用的时候有权获取位置数据;后者会得到跟之前 iOS 一样的后台位置服务。

if CLLocationManager.authorizationStatus() == .NotDetermined {
    manager.requestAlwaysAuthorization()
}

或者

if CLLocationManager.authorizationStatus() == .NotDetermined {
    manager.requestWhenInUseAuthorization()
}

因为这是异步的,应用不能立即开始使用位置服务。取而代之的是,应用必须实现 locationManager:didChangeAuthorizationStatus 的 delegate 方法,这个方法会在用户改变权限状态的时候调用。

如果用户之前已经授权了位置服务,那么在每次位置管理器被初始化,并且 delegate 被设置了相应的权限状态的情况下这个代理方法仍然会被调用。这使得一个单一的代码路径使用定位服务更为方便。

func locationManager(manager: CLLocationManager!,
                     didChangeAuthorizationStatus status: CLAuthorizationStatus)
{
    if status == .AuthorizedAlways || status == .AuthorizedWhenInUse {
        manager.startUpdatingLocation()
        // ...
    }
}

描述字符串

想在 iOS 8 中使用定位,另一个改变是必须的。在这之前,应用可以选择性的在 Info.plist 中包含 ‘NSLocationUsageDescription’ 的关键字。这个值是一个纯文本的字符串,向用户说明了应用预期要使用位置服务。现在这个值被拆分成了两个不同的关键字(NSLocationWhenInUseUsageDescriptionNSLocationAlwaysUsageDescription),而且是必填的;如果你不添加对应的关键字就去调用 requestWhenInUseAuthorizationrequestAlwaysAuthorization,那么将不会有任何的弹出提示给用户。

Core Location Always Authorization

Core Location When In Use Authorization

获取多个权限

另一个值得注意的细节是授权的弹出框会只显示一次。在 CLLocationManager.authorizationStatus() 返回除 NotDetermined 之外的值之后,不管调用 requestWhenInUseAuthorization()requestAlwaysAuthorization() 都不会有一个 UIAlertController 显示出来了。在用户最初的选择之后,唯一改变授权的方式是到 Settings.app 或者到隐私设置,又或者是应用自己的设置页面。

旧的授权机制各种不方便的情况下,现在让应用在它的生存周期内询问不论是“使用期间”还是“始终”的权限的机制明显复杂多了,更不方便了。为了缓解这一点,Apple 引入了一个字符串常量,UIApplicationOpenSettingsURLString,它存储了一个 URL 用来打开当前应用在 Settings.app 对应的页面。

下面的例子显示了如何在应用里弹出两种类型的权限获取窗口,如果你的应用打算获取始终的权限的话,可以参考一下。

switch CLLocationManager.authorizationStatus() {
    case .Authorized:
        // ...
    case .NotDetermined:
        manager.requestWhenAlwaysAuthorization()
    case .AuthorizedWhenInUse, .Restricted, .Denied:
        let alertController = UIAlertController(
            title: "Background Location Access Disabled",
            message: "In order to be notified about adorable kittens near you, please open this app's settings and set location access to 'Always'.",
            preferredStyle: .Alert)

        let cancelAction = UIAlertAction(title: "Cancel", style: .Cancel, handler: nil)
        alertController.addAction(cancelAction)

        let openAction = UIAlertAction(title: "Open Settings", style: .Default) { (action) in
            if let url = NSURL(string:UIApplicationOpenSettingsURLString) {
                UIApplication.sharedApplication().openURL(url)
            }
        }
        alertController.addAction(openAction)

        self.presentViewController(alertController, animated: true, completion: nil)
}

Core Location Settings Alert

Core Location Settings Location Never

Core Location Settings Location Always

向后兼容

所有这些新的 API 都只支持 iOS 8。对于要支持 iOS 7 或之前 iOS 版本的应用,则必须维护两部分代码,一个是为 iOS 8 获取权限的,同时还需要维护之前的获取位置更新的方法。一个简单的实现会看上去像下面这样:

func triggerLocationServices() {
    if CLLocationManager.locationServicesEnabled() {
        if self.manager.respondsToSelector("requestWhenInUseAuthorization") {
            manager.requestWhenInUseAuthorization()
        } else {
            startUpdatingLocation()
        }
    }
}

func startUpdatingLocation() {
    manager.startUpdatingLocation()
}

// MARK: - CLLocationManagerDelegate

func locationManager(manager: CLLocationManager!, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
    if status == .AuthorizedWhenInUse || status == .Authorized {
        startUpdatingLocation()
    }
}

建立用户的信任

现在在 iOS 8 上有一个共同的变化趋势是:它们更容易得到用户的信任。

显式的请求的授权,鼓励应用程序在用户试图做一些事情之前不请求授权。这包括了用使用说明来很清晰的解释为什么你需要的访问位置信息,以及应用程序将如何使用它。“使用期间” 和 “始终” 的区别授权,使用户感到轻松,因为应用只得到了需要的数据。

当然,这些新的 API 并不能阻止应用做跟之前一样的事。所有应用 “需要” 针对支持 iOS 8 的事只是添加对 useAlwaysAuthorization 的调用,以及添加一个全局适用的字符串。但随着这些新的变化, Apple 正在传达一个重要的信息,那就是你应该尊重你的用户。一旦用户习惯了像这样尊重用户隐私的应用程序,不难想象的是,不负责任地使用定位服务 的应用会在 App Store 得到更多负面的评分。

室内定位追踪

如果一个人仔细阅读了 CoreLocation.framework 的 API 改动文件,会发现最令人费解的改动之一是引进了 CLFloor,一个新的只有相当简单的接口对象:

class CLFloor : NSObject {
    var level: Int { get }
}
@interface CLFLoor : NSObject
@property(readonly, nonatomic) NSInteger level
@end

就是这样。仅一个属性,一个整形值来表示当前位置处于建筑物的第几层楼。

欧洲人肯定会很高兴的发现一楼是用 ‘1’ 表示的, 而不是 ‘0’。

CLLocationManager 返回的 CLLocation 对象可能包括一个 floor 属性,但如果你是写一个使用定位服务的示例应用程序,你会发现 CLLocation 对象的 floor 属性总是 nil

这是因为该API的变化只是 iOS8 中引入的室内定位跟踪这个大功能的冰山一角。对于大型空间的应用开发,例如艺术博物馆或百货公司这种, Apple 现在已经有结合了无线,GPS,蜂窝,和 室内定位数据的内置 Core Location API 支持 IPS

也就是说,这个新功能的信息令人吃惊的来之不易。该项目目前被很好的严格限制访问了,仅允许已通过从 Apple Maps Connect 申请的程序。关于该项目的的有限信息是在今年的WWDC(Session 708: Taking Core Location Indoors)被初露头角的,但大部分的后台细节都被隐藏在被关上的门之后了。对于大多数的我们来说,别无选择的,只能打消没有用的好奇心。

CLVisit

很多应用程序,使用位置监控的原因是确定用户是否在某个确定的地方。概念上讲,你在想的是诸如“地方”或“访问”的名词术语,而不是原始的 GPS 坐标。

然而,除非你可以得益于使用区域监视(被限制在一个相对小的数量的区域)或室内定位( iBeacon )测距(这要求把室内定位硬件真正安装在一个空间内),否则用 Core Location 的后台监控工具并不是非常适合。开发一个登记应用或像 Moves 这样的全面的日志应用,位置监控和花费很多时间做特定处理一般意味着消耗大量的电量。

在 iOS 8 里, Apple 曾试图通过引进 CLVisit 来解决这个问题, 这是一种新型的后台位置的监控。一个 CLVisit 表示该用户已经处于某个位置的时间长度,包括一个坐标和开始/结束的时间戳。

理论上讲,使用访问监控并不比任何其他后台定位跟踪做更多的事。简单地调用 manager.startMonitoringVisits() 将启用后台访问跟踪,假设用户同意授权你的应用程序“始终”的使用权限。一旦启动,你的应用程序将在有位置更新的时候在后台被唤醒,不像基本的定位监控,如果系统有个访问更新的队列(通常可以使更新延迟),你的 delegate 方法将被调用多次,每个单一的访问调用一次,而不是一个包含 CLLocation 对象的数组调用 locationManager:didReceiveUpdates:。调用 manager.stopMonitoringVisits() 会停止跟踪。

处理访问

每个 CLVisit 对象包含了一些基本属性:平均坐标,水平精度和到达日期和离开时间。

每次一个访问被追踪到,CLLocationManagerDelegate 可能会被告知两次:一次是在用户刚抵达一个新的地方的时候,以及当用户离开的时候。你可以通过检查 departureDate 属性来分辨它们; NSDate.distantFuture() 的离开时间意味着用户还在那儿。

func locationManager(manager: CLLocationManager!, didVisit visit: CLVisit!) {
    if visit.departureDate.isEqualToDate(NSDate.distantFuture()) {
        // User has arrived, but not left, the location
    } else {
        // The visit is complete
    }
}

实现须知

至少到 iOS 8.1,CLVisit 还不是那么精确。开始和结束时间一般有一两分钟的误差,是否访问某个地方的线路边际有点模糊。在咖啡店的角落躲一分钟可能不会触发访问,但在等一个特别长的红绿灯的时候却有可能触发。很可能 Apple 将在操作系统的后续升级的时候提升访问检测的质量,但现在如果你的应用程序对访问检测的精度要求很高的话,你最好不要依赖 CLVisit


除非另有声明,本文采用知识共享「署名-非商业性使用 3.0 中国大陆」许可协议授权。