iOS地图与传感器开发全解析
1. 地图功能概述
在iOS开发中,地图功能是许多应用的重要组成部分。通过相关的API,我们可以实现地理编码、搜索和导航等功能。
1.1 显示当前位置
若要让地图应用显示设备的当前位置,可使用
MKMapItem
的类方法
mapItemForCurrentLocation
。此方法不会尝试确定设备的位置,也不包含任何位置信息,仅生成一个
MKMapItem
对象。当将该对象发送到地图应用时,地图应用会尝试确定并显示设备的位置。示例代码如下:
let mi = MKMapItem.forCurrentLocation()
mi.openInMaps(launchOptions:[
MKLaunchOptionsMapTypeKey: MKMapType.standard.rawValue
])
1.2 地图服务
Map Kit为应用提供了三种涉及网络查询的服务,这些服务需要时间,且可能因网络和服务器可用性而失败,结果也可能存在不确定性。因此,它们都涉及一个在主线程上异步回调的完成函数。这三种服务分别为:
-
地理编码(Geocoding)
:实现街道地址与坐标之间的转换。例如,查询当前所在地址,或查询家庭地址的坐标。
-
搜索(Searching)
:对自然语言搜索进行可能匹配项的查找。例如,查找附近的泰国餐厅。
-
导航(Directions)
:查找从源位置到目标位置的逐向导航说明和路线映射。
完成函数会接收一个响应对象和一个错误对象,它们都被包装在可选类型中。若响应对象为
nil
,错误对象会告知问题所在。
除了应用自身提供这些服务外,还可请求地图应用提供。具体做法是构建一个URL,并调用
UIApplication
的
open(_:options:completionHandler:)
方法。关于该URL的结构,可参考苹果文档存档中“Apple URL Scheme Reference”的“Map Links”章节。
1.3 地理编码
地理编码功能封装在
CLGeocoder
类中,可调用以下方法:
-
geocodeAddressString(_:completionHandler:)
-
geocodePostalAddress(_:completionHandler:)
第二个方法需要一个来自
Contacts
框架的
CNPostalAddress
对象,因此需要导入
Contacts
。若操作成功,响应将是一个
CLPlacemark
对象数组,按从最佳到最差的猜测顺序排列;若非常顺利,数组将仅包含一个
CLPlacemark
对象。
CLPlacemark
可用于初始化
MKPlacemark
,
MKPlacemark
是
CLPlacemark
的子类,采用了
MKAnnotation
协议,因此适合直接传递给
MKMapView
进行显示。
以下是一个简单示例,允许用户在
UISearchBar
中输入地址,并在
MKMapView
中显示:
guard let s = searchBar.text else { return }
let geo = CLGeocoder()
geo.geocodeAddressString(s) { placemarks, error in
guard let placemarks = placemarks else { return }
let p = placemarks[0]
let mp = MKPlacemark(placemark:p)
self.map.addAnnotation(mp)
self.map.setRegion(
MKCoordinateRegion(center:mp.coordinate,
latitudinalMeters:1000, longitudinalMeters:1000),
animated: true)
}
默认情况下,生成的注释标题包含一个格式良好的描述地址的字符串。
反向地理编码则是从坐标(实际上是
CLLocation
对象,可从其他地方获取,或使用
init(latitude:longitude:)
构造)开始,调用
reverseGeocodeLocation(_:completionHandler:)
方法获取相应地址。地址通过
CLPlacemark
的
postalAddress
属性表示,这是一个
CNPostalAddress
对象,因此需要导入
Contacts
。可通过
CNPostalAddress
获取街道、城市、州等属性,也可使用
CNPostalAddressFormatter
对地址进行良好格式化。此外,还可直接查询
CLPlacemark
的属性,如
subthoroughfare
(门牌号)、
thoroughfare
(街道名称)、
locality
(城镇)和
administrativeArea
(州)。
以下是反向地理编码的示例,假设
MKMapView
已在跟踪用户,可获取用户位置并查询相应地址:
guard let loc = self.map.userLocation.location else { return }
let geo = CLGeocoder()
geo.reverseGeocodeLocation(loc) { placemarks, error in
guard let ps = placemarks, ps.count > 0 else {return}
let p = ps[0]
if let addy = p.postalAddress {
let f = CNPostalAddressFormatter()
print(f.string(from: addy))
}
}
1.4 搜索
MKLocalSearch
类以及
MKLocalSearch.Request
和
MKLocalSearch.Response
可让我们请求服务器进行自然语言搜索。与正向地理编码相比,这种搜索不太正式,可通过名称或描述搜索兴趣点。对于某些类型的搜索,通过设置请求的区域来限制感兴趣的区域可能会很有用。
以下是一个搜索附近泰国餐厅并在地图上显示的示例:
guard let loc = self.map.userLocation.location else { return }
let req = MKLocalSearch.Request()
req.naturalLanguageQuery = "Thai restaurant"
req.region = MKCoordinateRegion(center: loc.coordinate,
span: MKCoordinateSpan(latitudeDelta:1, longitudeDelta:1))
let search = MKLocalSearch(request:req)
search.start { response, error in
guard let response = response else { print(error); return }
self.map.showsUserLocation = false
let mi = response.mapItems[0] // I'm feeling lucky
let place = mi.placemark
let loc = place.location!.coordinate
let reg = MKCoordinateRegion(center:loc,
latitudinalMeters:1200, longitudinalMeters:1200)
self.map.setRegion(reg, animated:true)
let ann = MKPointAnnotation()
ann.title = mi.name
ann.subtitle = mi.phoneNumber
ann.coordinate = loc
self.map.addAnnotation(ann)
}
MKLocalSearchCompleter
可让我们使用
MKLocalSearch
的远程数据库,在用户输入搜索查询时提供补全建议。
1.5 导航
MKDirections
类以及
MKDirections.Request
和
MKDirections.Response
用于查找两个以
MKMapItem
对象表示的位置之间的步行或驾车导航。生成的
MKDirections.Response
包含一个
MKRoute
对象数组,每个
MKRoute
包含一个适合在地图上作为覆盖层显示的
MKPolyline
,以及一个
MKRoute.Step
对象数组,每个
MKRoute.Step
提供自己的
MKPolyline
、导航说明和距离。
MKDirections.Response
还有自己的源和目标
MKMapItem
,可能与初始的不同。
以下是一个继续前面泰国餐厅示例的导航示例:
// ... same as before up to this point ...
let mi = response.mapItems[0] // I'm still feeling lucky
let req = MKDirectionsRequest()
req.source = MKMapItem.forCurrentLocation()
req.destination = mi
let dir = MKDirections(request:req)
dir.calculate { response, error in
guard let response = response else { print(error); return }
let route = response.routes[0] // I'm feeling insanely lucky
let poly = route.polyline
self.map.addOverlay(poly)
for step in route.steps {
print("After \(step.distance) meters: \(step.instructions)")
}
}
逐向导航说明会显示在控制台中,在实际应用中,通常会在应用界面中显示这些说明。若要在地图视图中绘制路线,需要实现
mapView(_:rendererFor:)
方法,示例如下:
func mapView(_ mapView: MKMapView,
rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let overlay = overlay as? MKPolyline {
let r = MKPolylineRenderer(polyline:overlay)
r.strokeColor = UIColor.blue.withAlphaComponent(0.8)
r.lineWidth = 2
return r
}
return MKOverlayRenderer()
}
此外,还可调用
calculateETA(completionHandler:)
方法让
MKDirections
估算到达时间,部分公共交通系统也支持到达时间估算,还可让地图应用显示公交导航地图。
2. 传感器概述
设备可能包含用于感知周围环境的硬件,如确定自身位置、方向和运动状态。
2.1 传感器信息获取
- 位置信息 :通过Core Location框架,利用设备的Wi-Fi、蜂窝网络和GPS功能,获取设备当前位置以及位置随时间的变化信息。同时,借助磁力计获取设备相对于北方的方向信息。
-
运动和姿态信息
:通过
UIEvent类(用于设备摇晃检测)和Core Motion框架,利用加速度计获取设备速度和姿态的变化信息。Core Motion框架还可结合陀螺仪(若设备有)和磁力计,提供更高的精度。此外,设备可能有额外的芯片用于分析和记录用户的活动,如步行或跑步,甚至有气压计用于报告海拔变化,这些信息也可通过Core Motion框架获取。
2.2 开发挑战
在编写利用这些传感器的代码时,会面临一些挑战:
-
硬件差异
:不同设备的硬件不同。若不想对应用运行的设备施加严格限制,代码需能在当前设备缺少某些功能时优雅地失败,可能仅提供应用部分完整功能。
-
传感器不稳定性
:某些传感器可能会出现暂时的不足。例如,Core Location可能因无法“看到”基站、GPS卫星或两者兼而有之,而无法确定设备位置。而且,有些传感器需要“预热”时间,初始获取的值可能无效。因此,需要灵活应对外部环境的变化,以给用户提供良好的应用体验。
-
电池消耗
:所有传感器的使用都会在不同程度上消耗电池电量,有时消耗程度还相当大。因此,需要在满足用户对应用便利性和实用性的需求与避免因快速耗尽设备电池电量而让用户感到意外和厌烦之间做出权衡。
2.3 Core Location框架
Core Location框架(导入
CoreLocation
)为设备提供确定和报告其位置的功能(位置服务)。它利用三种传感器:
|传感器|工作原理|特点|
| ---- | ---- | ---- |
|Wi-Fi|若设备开启Wi-Fi,会扫描附近的Wi-Fi网络,并与在线数据库进行比较。|较常用,但精度相对较低。|
|Cell|若设备具备蜂窝网络功能且未关闭,会检测附近的电话基站,并与在线数据库进行比较。|精度一般,受基站分布影响。|
|GPS|若设备有GPS功能,可从GPS卫星获取位置信息。|最精确的位置传感器,但获取定位所需时间最长,在某些情况下可能失败,如室内或高楼林立的城市中,设备无法“看到”足够的天空。|
Core Location会自动使用设备可用的任何功能,只需请求设备的位置即可。同时,它允许指定所需的定位精度,追求更高的精度可能需要更多时间。
为了帮助测试依赖设备位置的代码,Xcode允许模拟设备位于地球上的特定位置。可通过模拟器的“Debug → Location”菜单输入位置,在Scheme编辑器的“Options”下设置默认位置,以及使用“Debug → Simulate Location”菜单切换不同位置。可设置内置位置,也可提供包含航点的标准GPX文件。还可将位置设置为“None”,测试无位置信息时的情况。
2.4 位置管理器、委托和授权
使用Core Location需要一个位置管理器对象,即
CLLocationManager
的实例。该对象需在主线程上创建并持续保留。常见策略是选择一个在应用生命周期内持续存在的实例,如应用委托或根视图控制器,并使用位置管理器初始化其实例属性:
let locman = CLLocationManager()
位置管理器通常需要一个委托(
CLLocationManagerDelegate
)才能发挥作用。不建议更改位置管理器的委托,因此应在位置管理器的生命周期早期设置一次。委托需要是一个与位置管理器一起持续存在的实例。例如,若
locman
是根视图控制器的常量属性,可将根视图控制器设置为其委托,建议在根视图控制器的初始化器中尽早完成此操作:
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
self.locman.delegate = self
}
首次开始跟踪设备位置时,必须明确请求用户授权。有两种类型的授权:
-
使用时授权(When In Use)
:允许应用在运行时进行基本的位置确定。
-
始终授权(Always)
:允许应用使用Core Location的所有模式和功能,包括应用未运行时也能运行的功能。
若请求始终授权,需要向用户说明启用这些额外功能的必要性,因为授予此授权意味着用户的位置可能在其不知情的情况下被跟踪,同时也会消耗一定的设备电池电量。因此,在请求始终授权时,必须让用户也能选择使用时授权。请求始终授权的授权请求警报中会包含一个按钮,允许用户选择使用时授权。希望获得始终授权的应用应做好仅使用时授权也能正常运行的准备。
需要注意的是,用户可随时更改应用的授权状态。通过设置应用,用户可开启或关闭应用的位置访问权限。若应用曾请求过始终授权,用户将有三个选项:从不、使用应用时和始终,并可自由切换。
若要请求始终授权,苹果建议先单独请求使用时授权。若用户授予使用时授权,后续再请求始终授权,系统会为你显示一个特殊的“过渡”授权请求警报。但苹果自己的应用不一定遵循此协议。
此外,用户还可完全关闭位置服务。若位置服务关闭,而你仍尝试使用Core Location,系统可能会弹出一个警报,提示用户切换到设置应用以开启位置服务。
CLLocationManager
的类方法
locationServicesEnabled
可报告位置服务是否整体关闭。若关闭,一种可能的策略是仍调用位置管理器的
startUpdatingLocation
方法。虽然尝试获取设备位置会失败,但此失败可能会让用户看到系统警报:
if !CLLocationManager.locationServicesEnabled() {
self.locman.startUpdatingLocation()
return
}
位置服务启用后,可调用
CLLocationManager
的类方法
authorizationStatus
了解应用的实际授权状态。有两种授权类型,因此有两种状态表示已获得授权:
.authorizedWhenInUse
和
.authorizedAlways
。若状态为
.notDetermined
,可通过调用
requestWhenInUseAuthorization
或
requestAlwaysAuthorization
实例方法,请求系统为你显示授权请求警报。同时,应用的
Info.plist
文件中必须有相应的条目,提供授权请求警报的内容,分别是“Privacy — Location When In Use Description”(
NSLocationWhenInUseUsageDescription
)和“Privacy — Location Always and When In Use Usage Description”(
NSLocationAlwaysAndWhenInUseUsageDescription
)。(“Privacy — Location Always Usage Description”(
NSLocationAlwaysUsageDescription
)在iOS 11及以后版本中不再使用。)
奇怪的是,
requestWhenInUseAuthorization
和
requestAlwaysAuthorization
都不接受完成函数,代码会继续执行。若调用
requestWhenInUseAuthorization
后立即调用
startUpdatingLocation
尝试跟踪设备位置,可能会因用户授予授权而成功,但也可能失败。Core Location API没有提供简单的方法,让你在知道授权请求结果后再继续执行。
不过,每当用户更改应用的授权状态时,无论是在授权请求警报中授予授权,还是在设置应用中提供授权,位置管理器委托的
locationManager(_:didChangeAuthorization:)
方法都会被调用。因此,可在获得授权前存储想要执行的操作,在获得授权后执行该操作。
以下是一种实现此功能的策略:创建一个实用类
ManagerHolder
,它创建并保留位置管理器,在需要时请求授权,并存储获得授权后要调用的函数:
class ManagerHolder {
let locman = CLLocationManager()
var doThisWhenAuthorized : (() -> ())?
func checkForLocationAccess(always:Bool = false,
andThen f: (()->())? = nil) {
// no services? try to get alert
guard CLLocationManager.locationServicesEnabled() else {
self.locman.startUpdatingLocation()
return
}
let status = CLLocationManager.authorizationStatus()
switch status {
case .authorizedWhenInUse:
if always { // try to step up
self.doThisWhenAuthorized = f
self.locman.requestAlwaysAuthorization()
} else {
f?()
}
case .authorizedAlways:
f?()
case .notDetermined:
self.doThisWhenAuthorized = f
always ?
self.locman.requestAlwaysAuthorization() :
self.locman.requestWhenInUseAuthorization()
case .restricted: break // do nothing
case .denied: break // do nothing, or beg for authorization
}
}
}
这个实用类封装了位置管理器的管理和授权功能,提供了灵活性。可在任何地方实例化位置管理器,并确保其得到正确管理。一种做法是将
ManagerHolder
实例附加到根视图控制器,也可将其附加到应用委托,或两者都附加。
以下是将
ManagerHolder
实例附加到根视图控制器的示例:
class ViewController: UIViewController, CLLocationManagerDelegate {
let managerHolder = ManagerHolder()
var locman : CLLocationManager {
return self.managerHolder.locman
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
self.locman.delegate = self
}
// ...
}
综上所述,在iOS开发中,地图和传感器功能为应用增添了丰富的交互性和实用性。通过合理运用Core Location和Core Motion框架,开发者可以为用户提供更加个性化和便捷的体验。但同时也需要注意处理硬件差异、传感器不稳定性和电池消耗等问题,以确保应用的稳定性和用户满意度。
2.5 位置管理流程总结
为了更清晰地展示使用Core Location进行位置管理的流程,下面给出一个mermaid格式的流程图:
graph TD;
A[开始] --> B{位置服务是否启用};
B -- 是 --> C{应用授权状态};
B -- 否 --> D[尝试触发系统警报];
D --> A;
C -- .notDetermined --> E{请求授权类型};
E -- 始终授权 --> F[请求始终授权];
E -- 使用时授权 --> G[请求使用时授权];
C -- .authorizedWhenInUse --> H{是否需要始终授权};
H -- 是 --> F;
H -- 否 --> I[执行操作];
C -- .authorizedAlways --> I;
C -- .restricted --> J[不做处理];
C -- .denied --> K[可请求重新授权];
F --> L{授权结果};
L -- 成功 --> I;
L -- 失败 --> K;
G --> M{授权结果};
M -- 成功 --> I;
M -- 失败 --> K;
2.6 代码示例总结
以下是使用Core Location框架进行位置管理的关键代码示例总结:
1.
创建位置管理器
let locman = CLLocationManager()
- 设置委托
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
self.locman.delegate = self
}
- 检查位置服务是否启用
if !CLLocationManager.locationServicesEnabled() {
self.locman.startUpdatingLocation()
return
}
- 检查授权状态并请求授权
class ManagerHolder {
let locman = CLLocationManager()
var doThisWhenAuthorized : (() -> ())?
func checkForLocationAccess(always:Bool = false,
andThen f: (()->())? = nil) {
guard CLLocationManager.locationServicesEnabled() else {
self.locman.startUpdatingLocation()
return
}
let status = CLLocationManager.authorizationStatus()
switch status {
case .authorizedWhenInUse:
if always {
self.doThisWhenAuthorized = f
self.locman.requestAlwaysAuthorization()
} else {
f?()
}
case .authorizedAlways:
f?()
case .notDetermined:
self.doThisWhenAuthorized = f
always ?
self.locman.requestAlwaysAuthorization() :
self.locman.requestWhenInUseAuthorization()
case .restricted: break
case .denied: break
}
}
}
- 将ManagerHolder实例附加到根视图控制器
class ViewController: UIViewController, CLLocationManagerDelegate {
let managerHolder = ManagerHolder()
var locman : CLLocationManager {
return self.managerHolder.locman
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
self.locman.delegate = self
}
// ...
}
3. 地图与传感器结合应用示例
3.1 需求场景
假设我们要开发一个旅游应用,用户可以在地图上搜索附近的景点,并获取前往景点的导航信息。同时,应用会根据用户的位置和运动状态提供一些个性化的推荐。
3.2 实现步骤
-
初始化地图和位置管理器
-
创建
MKMapView和CLLocationManager实例。 - 设置位置管理器的委托,并请求授权。
-
创建
import UIKit
import MapKit
import CoreLocation
class ViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
let mapView = MKMapView()
let locationManager = CLLocationManager()
let managerHolder = ManagerHolder()
override func viewDidLoad() {
super.viewDidLoad()
setupMapView()
setupLocationManager()
}
private func setupMapView() {
mapView.frame = view.bounds
view.addSubview(mapView)
mapView.delegate = self
}
private func setupLocationManager() {
locationManager.delegate = self
managerHolder.checkForLocationAccess(andThen: { [weak self] in
self?.locationManager.startUpdatingLocation()
})
}
}
-
搜索附近景点
-
使用
MKLocalSearch进行自然语言搜索,查找附近的景点。
-
使用
func searchForNearbyAttractions() {
guard let currentLocation = locationManager.location else { return }
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = "景点"
request.region = MKCoordinateRegion(center: currentLocation.coordinate, span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
let search = MKLocalSearch(request: request)
search.start { [weak self] (response, error) in
guard let response = response else {
print(error?.localizedDescription ?? "搜索失败")
return
}
for mapItem in response.mapItems {
let placemark = mapItem.placemark
let annotation = MKPointAnnotation()
annotation.title = mapItem.name
annotation.coordinate = placemark.coordinate
self?.mapView.addAnnotation(annotation)
}
}
}
-
获取导航信息
-
当用户选择一个景点时,使用
MKDirections获取前往该景点的导航信息。
-
当用户选择一个景点时,使用
func getDirections(to destination: MKMapItem) {
let request = MKDirections.Request()
request.source = MKMapItem.forCurrentLocation()
request.destination = destination
let directions = MKDirections(request: request)
directions.calculate { [weak self] (response, error) in
guard let response = response else {
print(error?.localizedDescription ?? "获取导航信息失败")
return
}
let route = response.routes[0]
self?.mapView.addOverlay(route.polyline)
for step in route.steps {
print("行驶 \(step.distance) 米后:\(step.instructions)")
}
}
}
-
根据用户运动状态提供推荐
- 使用Core Motion框架获取用户的运动状态,如步行、跑步等。
- 根据运动状态提供不同的推荐,如步行时推荐附近的小吃店,跑步时推荐公园等。
import CoreMotion
class MotionManager {
let motionManager = CMMotionActivityManager()
func startMonitoringActivity() {
motionManager.startActivityUpdates(to: OperationQueue.main) { [weak self] (activity) in
guard let activity = activity else { return }
if activity.walking {
// 推荐附近小吃店
self?.searchForNearbySnackShops()
} else if activity.running {
// 推荐附近公园
self?.searchForNearbyParks()
}
}
}
private func searchForNearbySnackShops() {
// 实现搜索附近小吃店的逻辑
}
private func searchForNearbyParks() {
// 实现搜索附近公园的逻辑
}
}
3.3 总结
通过结合地图和传感器功能,我们可以开发出功能丰富、个性化的应用。在开发过程中,需要注意处理好授权、错误处理和性能优化等问题,以提供良好的用户体验。
4. 总结与展望
4.1 总结
本文详细介绍了iOS开发中地图和传感器的相关功能和使用方法。在地图功能方面,涵盖了地理编码、搜索、导航等核心功能,并给出了具体的代码示例。在传感器方面,重点介绍了Core Location和Core Motion框架,包括位置管理、授权处理以及运动状态监测等内容。同时,通过一个旅游应用的示例,展示了如何将地图和传感器功能结合起来,开发出实用的应用。
4.2 展望
随着技术的不断发展,地图和传感器功能将会越来越强大。未来,我们可以期待更多的创新应用,如基于增强现实(AR)的地图导航、更加精准的运动监测和健康分析等。开发者可以充分利用这些技术,为用户带来更加丰富和便捷的体验。同时,也需要关注隐私保护和数据安全等问题,确保用户的信息得到妥善处理。
超级会员免费看
81

被折叠的 条评论
为什么被折叠?



