转载

iOS 9 by Tutorials 笔记(十四)

Chapter 14 Location and Mapping

尽管 iOS 的地图服务被大家广为诟病,但 Apple 每年都会持续改进,iOS 9 也不例外,MapKit 和 Core Location 迎来一大波更新。

其中最有用的一个改进就是在地图上增加了行程导航,本章将学习这些新特性:

  • 自定义地图外观的新方法
  • 行程导航
  • 估计行程的时间
  • 使用 Core Location 进行单个位置更新

这一章,Café Transit 的示例应用程序是为所有的咖啡爱好者开发的。它可以帮助你寻找那些称赞的咖啡。目前,它只显示附近的的一小撮咖啡馆。而当你完成了这一章,App 会显示大量的有用的信息,包括每个咖啡店,评级,定价信息和开放时间。还会提供到指定咖啡店的行程导航信息,以及告诉你什么时候出发和什么时候到达。

Getting started

熟悉下 Demo 程序

iOS 9 by Tutorials 笔记(十四)

  • ViewController.swift
    • setupMap() 设置地图并限定显示区域
    • addMapData() 从 model 中载入地图注释信息
  • CoffeeShop.swift 咖啡馆的 model 信息,并负责从 plist 载入信息
  • CoffeeShopPinDetailView.swiftCoffeeShopPinDetailView.xib 负责表示自定义的注释,该注释将会显示评分、价格信息和营业时间

Customizing maps

iOS 9 之前你只能通过编程的方式 开启/关闭 地图上的特定建筑物。iOS 9 介绍了三个新的 Boolen 属性,让你 开启/关闭 地图上的 compass -罗盘, scale bar -比例尺, traffic -交通流量

iOS 9 by Tutorials 笔记(十四)

我们可以为 Café Transit 开启比例尺,在 setupMap() 里开启

mapView.showsScale = true   

iOS 9 by Tutorials 笔记(十四)

随着对地图的缩放,比例尺也会跟着变化

Customizing map pins

在 iOS 9 中,苹果用 pinTintColor 替代了 pincolor ,新的属性允许你将标记大头针设置成自己喜欢的颜色了(原属性只能设红、绿、紫三种颜色)

我们来将五星评价的餐厅在地图上设为黄色,其余的设为棕色

if annotation.coffeeshop.rating.value == 5 {     annotationView!.pinTintColor =     UIColor(red:1, green:0.79, blue:0, alpha:1) } else {   annotationView!.pinTintColor =     UIColor(red:0.419, green:0.266, blue:0.215, alpha:1) } 

iOS 9 by Tutorials 笔记(十四)

Customizing annotation callouts

咖啡馆在地图上以大头针形式标注,且当你点击大头针 annotation view 时,会显示一个标注信息 callout ,展示关于该咖啡馆的额外的信息

iOS 9 by Tutorials 笔记(十四)

在 iOS 9 之前,你想要在 annotation view 里添加一个自定义 View 并不是一件容易的事情。但现在 iOS 9 让事情变得简单了。 MKAnnotationView 现在有一个新属性 detailCalloutAccessoryView 来展示这个 callouts ,并且该 View 并没什么限制。

Managing callout size

Callouts 将根据你的自定义视图的大小来调整自己的尺寸。你的自定义 callouts 可以利用下面两种方式:

  • 在你的自定义视图中使用 Auto Layout 来布局
  • 你可以覆盖 intrinsicContentSize 来定制你需要的尺寸

这里用了 CoffeeShopPinDetailView.XIB 来设计自定义的 Callout ,在 XIB 中我们使用 UIStackViewAuto Layout 来布局

自定义的 callouts 并不会铺满整个标注信息视图,他会显示一个标题和四周留白:

iOS 9 by Tutorials 笔记(十四)

没办法修改标题和四周的留白区域

Adding a custom callout accessory view

理论学习完了,现在我们来添加自定义的 callout,UI 已经在 CoffeeShopPinDetailView.xib 中设计好了

iOS 9 by Tutorials 笔记(十四)

打开 ViewController.swiftmapView(_:viewForAnnotation:) 里加入下面的方法:

let detailView = UIView.loadFromNibNamed(identifier) as!     CoffeeShopPinDetailView detailView.coffeeShop = annotation.coffeeshop   annotationView!.detailCalloutAccessoryView = detailView   

首先从 XIB 文件中载入 CoffeeShopPinDetailView ,然后为其分配当前的 coffeeshop ,最后设置 view 的 detailCalloutAccessoryView 属性即可。

iOS 9 by Tutorials 笔记(十四)

点击 Yelp 按钮会打开 Safari 将你带到该咖啡店在 Yelp 的评价主页,但时钟按钮现在还点不了,稍后我们来实现

Supporting time zones

我们上面添加的 callouts 包含一个标识,指示当前咖啡馆是否正在营业:

iOS 9 by Tutorials 笔记(十四)

static var timeZone = NSTimeZone(abbreviation: "PST")!  /// Calculates whether a coffee shop is currently open for business var isOpenNow: Bool {       let calendar = NSCalendar.currentCalendar()     let nowComponents = calendar.componentsInTimeZone(CoffeeShop.timeZone, fromDate: NSDate())     ... 

isOpenNow 是个计算属性,用来标识当前营业状态。这里使用了 NSDate() 得到当前时间,并转换成咖啡馆所在时区的时间,以此来判断咖啡馆现在是否开始营业了。

很简单不是吗?但我们观察这一句:

static var timeZone = NSTimeZone(abbreviation: "PST")!   

这里硬编码了时区 PST,虽然我们的 APP 只包含了三藩的咖啡馆,但如果能根据地理位置自动推断出对应的时区时间岂不是更赞!

iOS 9 为 MKMapItemCLPlacemark 添加了 timeZone 属性,我们利用该属性来得到符合当前地理位置的正确时间。

static func allCoffeeShops() -> [CoffeeShop] {       guard let path = NSBundle.mainBundle().pathForResource("sanfrancisco_coffeeshops", ofType: "plist"),       let array = NSArray(contentsOfFile: path) as? [[String : AnyObject]] else {         return [CoffeeShop]()     }      // 1     let shops = array.flatMap { CoffeeShop(dictionary: $0) }       .sort { $0.name < $1.name }     // 2     let first = shops.first!     let location = CLLocation(latitude: first.location.latitude,       longitude: first.location.longitude)     // 3     let geocoder = CLGeocoder()     geocoder.reverseGeocodeLocation(location) { (placemarks, _) in       if let placemark = placemarks?.first, timeZone =       placemark.timeZone {       self.timeZone = timeZone       }     }     return shops   } 
  1. 根据 plist 文件得到所有的咖啡馆
  2. 找出第一个咖啡馆的地理位置:经纬度
  3. 将经纬度转码成地理信息,在回调闭包得到 timeZone ,并设置为咖啡馆( CoffeeShop )的 timeZone

现在运行,你会发现每个咖啡馆会基于旧金山时间来显示是否营业,而不是你当地的时间。

在实际项目中,你需要判断每个咖啡馆的地理位置,因为他们可能分布在不同时区

Simulating your location

我们当前所有的咖啡馆都在旧金山,所以我们也要假装自己在旧金山。比较幸运的是 Xcode 很容易就能做到这一点

点击 CafeTransit scheme 选择 Edit Scheme

iOS 9 by Tutorials 笔记(十四)

勾选 Allow Location Simulation

iOS 9 by Tutorials 笔记(十四)

Making a single location request

在 iOS 9 之前,得到用户当前位置需要一个相当繁琐的过程,你要创建一个 CLLocationManager ,实现一些代理方法,然后调用 startUpdatingLocation() ,然后随着用户位置移动,会反复调用 location manager delegate 方法。如果一旦达到了期望的精度,你需要调用 stopUpdatingLocation() 来让 location manager 停止工作,不然你的手机电量会很快耗光。

在 iOS 9 将这些繁琐的过程封装成了一个方法: requestLocation() ,他仍然利用了 API 中的 delegate 回调方法,但不需要你手动控制开始结束了。你进需要设置期望的精度,然后 Core Location 会提供给你位置信息。他只调用一次 delegate 并且只返回一个位置。

理论听够了,来看实际例子

Adding a location manager

ViewController.swift 的类声明下添加两个对象:

lazy var locationManager = CLLocationManager()   // 用来存储用户位置 var currentUserLocation: CLLocationCoordinate2D?   

viewDidLoad() 中设置代理和期望精度:

locationManager.delegate = self   locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters   

下面我们来实现这个 CLLocationManagerDelegate 代理

// MARK:- CLLocationManagerDelegate extension ViewController: CLLocationManagerDelegate {     // 查看该 App 是否有权限查看用户位置信息,如果有,请求用户位置   func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {       if (status == CLAuthorizationStatus.AuthorizedAlways ||         status == CLAuthorizationStatus.AuthorizedWhenInUse) {       locationManager.requestLocation()     }   }   // 存储返回的第一个位置坐标   func locationManager(manager: CLLocationManager,     didUpdateLocations locations: [CLLocation]) {     currentUserLocation = locations.first?.coordinate   }   // 记录错误   func locationManager(manager: CLLocationManager,     didFailWithError error: NSError) {     print("Error finding location: +       /(error.localizedDescription)")   }  } 

现在你需要从其他地方调用 requestLocation()

下面在 ViewController.swift 添加一个私有方法用来获取用户位置:

private func requestUserLocation() {     // 在地图上显示用户位置   mapView.showsUserLocation = true   // 请求之前判断权限,有权限更新位置,无权限先鉴权   if CLLocationManager.authorizationStatus() == .AuthorizedWhenInUse {     locationManager.requestLocation()   } else {     locationManager.requestWhenInUseAuthorization()   } } 

注意,当你调用 requestWhenInUseAuthorization() 请求权限时,必须已经提前在 Info.plist 文件中,为 key: NSLocationWhenInUseUsageDescription 设置好了对应的键值。这个键值通常是一个字符串,会随鉴权请求弹窗一起展示给用户。

iOS 9 by Tutorials 笔记(十四)

我们让地图一出现在屏幕上就请求用户位置

override func viewDidAppear(animated: Bool) {     super.viewDidAppear(animated)   requestUserLocation() } 

最后我们在 MKMapViewDelegate 中将用户位置传递给选中的 annotation (咖啡馆大头针标记),为下一节交通导航路线做准备

func mapView(mapView: MKMapView,     didSelectAnnotationView view: MKAnnotationView) {   if let detailView = view.detailCalloutAccessoryView      as? CoffeeShopPinDetailView {     detailView.currentUserLocation = currentUserLocation   }  } 

Requesting transit directions

既然已经得到用户位置,现在让我们来添加前往指定咖啡馆的交通搭乘路线,这次在 CoffeeShopPinDetailView.swift 中添加一个 helper 方法

func openTransitDirectionsForCoordinates(     coord:CLLocationCoordinate2D) {   let placemark = MKPlacemark(coordinate: coord,     addressDictionary: coffeeShop.addressDictionary) // 1   let mapItem = MKMapItem(placemark: placemark)  // 2   let launchOptions = [MKLaunchOptionsDirectionsModeKey:     MKLaunchOptionsDirectionsModeTransit]  // 3   mapItem.openInMapsWithLaunchOptions(launchOptions)  // 4 } 
  1. 创建一个 MKPlacemark 用来存储你的坐标, Placemarks 通常有一个相对应的地址,而 coffee shop model 提供了基本的店名(从通讯录中)
  2. 用第一步的 placemark 初始化一个 MKMapItem (封装地图上某一点的相关信息)
  3. 指定导航模式,这里一共有三种可供设置:
    • MKLaunchOptionsDirectionsModeDriving 开车
    • MKLaunchOptionsDirectionsModeWalking 步行
    • MKLaunchOptionsDirectionsModeTransit 搭乘公共交通
  4. 以指定的导航模式打开地图,并显示导航线路

我们在 transitTapped() 中调用这个 helper 方法

@IBAction func transitTapped() {   openTransitDirectionsForCoordinates(coffeeShop.location) } 

运行,在地图上点击标记的咖啡馆大头针,弹出的 callout 视图中点按 train 图标,你就会直接进入公共交通导航界面

iOS 9 by Tutorials 笔记(十四)

Querying transit times

最后一个新特性是,MapKit 允许你查询公共交通行程信息。 MKETAResponse 类在 iOS 9 新增了下面一些有用的属性:

public var expectedTravelTime: NSTimeInterval { get }   @available(iOS 7.0, *) public var distance: CLLocationDistance { get }   @available(iOS 9.0, *) public var expectedArrivalDate: NSDate { get }   @available(iOS 9.0, *) public var expectedDepartureDate: NSDate { get }   @available(iOS 9.0, *) public var transportType: MKDirectionsTransportType { get }   

这些属性告诉你旅行的距离,时间以及出发时间和到达时间。

同样在地图上点击标记的咖啡馆大头针,弹出的 callout 视图中点按时钟图标,整个 view 会以动画的形式向上展示预估的出发时间和达到时间,下面让我们来实现这种效果:

还是在 CoffeeShopPinDetailView.swift 中,刚才的 helper 方法下面添加:

func requestTransitTimes() {     guard let currentUserLocation = currentUserLocation else { return   } // 1   let request = MKDirectionsRequest() // 2   let source = MKMapItem(placemark:     MKPlacemark(coordinate: currentUserLocation,     addressDictionary: nil))   let destination = MKMapItem(placemark:     MKPlacemark(coordinate: coffeeShop.location,     addressDictionary: nil)) // 3   request.source = source   request.destination = destination   request.transportType = MKDirectionsTransportType.Transit // 4   let directions = MKDirections(request: request)   directions.calculateETAWithCompletionHandler {     response, error in     if let error = error {       print(error.localizedDescription)     } else { // 5       self.updateEstimatedTimeLabels(response)     }   }  } 
  1. 一旦确认用户位置,初始化一个 MKDirectionsRequest 实例
  2. 创建两个 MKMapItem 实例,一个表示用户位置,另一个表示咖啡馆的位置。这里没有用到 addressDictionary 是因为我们只需要经纬度信息。
  3. 设置 MKDirectionsRequest 对象的源地址和目的地址,交通工具类型
  4. 用上面的 request 创建一个 MKDirections 对象实例,并执行 ETA 计算
  5. 最终结果将以闭包形式返回,如果收到一个成功的响应,则根据 response 更新在 view 上更新出发时间和到达时间

最后实现点按时钟图标获取行程时间的方法,依然是在 CoffeeShopPinDetailView.swift

@IBAction func timeTapped() {   if timeStackView.hidden {     animateView(timeStackView, toHidden: false)     requestTransitTimes()   } else {     animateView(timeStackView, toHidden: true)   } } 

现在,当你按下时钟(clock)图标,时间视图会向上滑出,并向苹果服务器发送计算请求,最终行程的计算结果会自动更新子时间视图上。

iOS 9 by Tutorials 笔记(十四)

-EOF-
正文到此结束
Loading...