iOS玩转地图

*初学地图时,觉得地图是个很高深的玩意儿,导航、定位、检索这得运用多少算法和核心动画的知识点啊,于是一直排斥 *

后来开始着手研究,也不敢直接去研究它底部怎么用,而是直接用的百度的SDK,然而用了更是觉得恶心,因为要添加很多静态库和包,导致一个小demo就几百M,想想有几个用户愿意下载一个地图就几百M啊,于是咨询过几个专门做这块的朋友,并查阅了很多这方面的资料,发现这洞真的很深。

** 但是想要运用做些APP里面导航、定位、追踪、检索、自定义大头针确是很简单的,基本掌握2个苹果原生的类 CoreLocation 和 MapKit 你就能把地图玩的很6了(如果想深究底层的看到这里请return)**

下面简单介绍下这2个类的使用,看完基本上APP上要用到地图的一些基本功能你都会做了,由于最近一直用的swift3.0,下面的代码都是用的swift3.0写的

CoreLocation

一、版本适配、授权

  • iOS8.0之前:
  • 在iOS6.0之后,苹果非常重视用户的隐私,只要APP访问用户的隐私都会进行弹框让用户授权(通讯录,相机,位置..健康)
  • 如果想要提高用户点击ok的几率,需要在info.plist文件中配置一个key:Privacy - Location Usage Description
  • 建议:key的值不要乱写,找PM(产品经理),PD(开发人员)要文案
  • 默认情况下,只能在前台获取用户的位置信息,如果在后台也想获取用户的位置信息,需要开启后台模式
  • iOS8.0之后
    想要获取用户的位置信息,需要主动请求授权(前台定位授权or前后台定位授权)

** 申请前台定位授权:**

  1. 必须在info.plist文件中配置对应的key NSLocationWhenInUseUsageDescription
  2. 默认情况下只能在前台获取用户的位置信息(用户进入软件才能获取位置信息)
  3. 如果想要在后台也能获取用户的位置信息,需要:开启后台模式 location updates
  4. 在后台获取用户的位置信息,系统会在顶部显示蓝色横幅,时时提示用户该app在获取你的位置信息,点击蓝色横幅则可以打开该app(这个是不好的,小心用户卸了你的APP)
    locationM.requestWhenInUseAuthorization()// 前台定位授权

** 申请前后台定位授权: **

  1. 不会出现蓝色横幅
  1. 必须在info.plist文件中配置对应的key NSLocationAlwaysUsageDescription
  2. 在前台和后台都能够获取用户的位置信息,在后台获取用户的位置信息,不需要开启后台模式
    locationM.requestAlwaysAuthorization()// 前后台定位授权
    可不用再background Modes里面勾选location updates
  • iOS9.0之后

申请前台定位授权:
1.需要开启后台模式: location updates
2.必须允许后台获取用户的位置信息(下面的版本适配代码)
3.在后台获取用户的位置信息,也会出现iOS8.0的那个蓝色横幅提示信息
注意点: 如果允许后台获取用户的位置信息,必须勾选后台模式,否则此代码会造成崩溃
if #available(iOS 9.0, *) {
locationM.allowsBackgroundLocationUpdates = true }

** 申请前后台定位授权:相对于iOS8.0,没有什么更新的东西**

二.监听状态

定位是使用CoreLocation这个框架实现的,而跟定位离不开关系的是位置管理者 CLLocationManager,使用位置管理者的时候,通常会提供一个懒加载的方法来保证他不被销毁.苹果给他提供了代理的接口,外界可以通过设置代理来获取他所能管理的所有东西和动态.下面介绍一些代理方法的功能:

/// 当获取到用户的位置的时候会来到该方法
/// - Parameters:
///   - manager: 位置管理者
///   - locations: 位置数组
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        print("定位到了")
}
/// 当授权状态发生改变的时候会来到该方法(授权状态不受开发人员控制,由用户控制)
/// - Parameters:
///   - manager: 位置管理者
///   - status: 授权状态( notDetermined, denied, restricted, authorizedAlways, authorizedWhenInUse)
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {}

注意:当用户自己拒绝APP对他进行位置定位的时或手机主人之前已经把手机的定位功能关闭时,status都是拒绝状态,如果手机的定位功能处于关闭状态,苹果会自动跳转到手机设置界面,但是如果是用户自己在打开APP时直接选择拒绝的,苹果不会自动跳转手机设置界面,需要开发者做友情提示用户自己去开启.


Snip20161111_1.png

** 手机上这2个选项由开发者在info.plist文件是否添加了这2个key:Privacy - Location Always Usage Description、Privacy - Location When In Use Usage Description,value决定弹框提醒是否允许授权上的提示语句 **

如果用户关闭了定位服务,就是真正拒绝,那么提示并弹框让他去设置

if CLLocationManager.locationServicesEnabled() { // 位置服务开启
                print("用户真正拒绝")
                
                if #available(iOS 8.0, *) {
                    //设置要打开的URL为“设置”
                    let url = URL(string: UIApplicationOpenSettingsURLString)!
                    if UIApplication.shared.canOpenURL(url) {//如果允许打开
                        //点击“设置”,打开“设置”,去允许定位
                        UIApplication.shared.openURL(url)
                    }

** 如果第一次没有点setting,点了cancel,后面不会有提醒去设置 **

三、参数设置

  • 1、设置多少米 获取用户的位置一次
    // 1: 获取1次 2: 获取111KM/100M次
    // 下一个位置相对于上一个位置大于100米,就会定位一次
    locationM.distanceFilter = 100

  • 2、设置定位精确度
    //AccuracyBestForNavigation : 最适合导航
    //AccuracyBest : 最好的
    //AccuracyNearestTenMeters : 附近10米
    //AccuracyHundredMeters : 附近100米
    //AccuracyKilometer : 附近1000米
    //AccuracyThreeKilometers : 附近3000米

** 注:定位的精确度越高,越消耗电,尽量给用户省电,在能够满足需求情况下,尽量使用低精确度 **
locationM.desiredAccuracy = kCLLocationAccuracyBest

四、开始定位方法选择

1、locationM.startUpdatingLocation()
* // 调用这个方法,就会不断地获取用户的位置信息
// 优点: 定位精确度高
// 缺点: 比较耗电,app必须运行着才能获取用户的位置信息

    // 标准定位服务(基于GPS/wifi/蓝牙/蜂窝基站)
    // 具体使用什么方式进行定位由苹果决定
    // GPS:定位最准确,缺点:如果被建筑物挡住,就检测不到了
    // wifi:GPS不能使用则使用wifi
    // 蜂窝基站: 不能使用wifi,则使用蜂窝基站
    // 蓝牙 : iwatch蓝牙连接手机

2、locationM.startMonitoringSignificantLocationChanges()
* // 监听重大位置改变(基于基站定位)
// 优点: 比较省电,就算app被完全杀死,也可以获取用户的位置信息,当用户的位置发生改变的时候,会自动启动该app在后台执行,来获取用户的位置信息
// 缺点: 定位精确度低,硬性要求必须有电话模块
// 请求一次精确度最高的用户位置信息
// 根据精确度来获取用户的位置信息
// 注意点:必须实现定位失败的代理方法,locationManager(_ manager: CLLocationManager, didFailWithError error: Error)
// 不能与startUpdatingLocation一起使用

五、CLLocation对象

位置对象的一些属性:

coordinate (CLLocationCoordinate2D): 经纬度信息,是个结构体,里面有经度和纬度。这个结构体可将鼠标在地图上点击的坐标点转成经纬度
将控件上面的点(CGPoint),转为经纬度

let coordinate = mapView.convert(point!, toCoordinateFrom: mapView)```

>altitude : 海拔
horizontalAccuracy : 水平精确度,如果为负数,代表位置不可用
verticalAccuracy : 垂直精确度,如果为负数,代表海拔不可用
course : 航向,如果为负数,代表航向不可用
speed : 速度,如果为负数,代表速度不可用
timestamp : 获取当前定位到的时间
distance(from location: CLLocation) : 计算2个位置之间的实际物理距离

如下在代理方法中,拿到位置对象所有属性:
```obj
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else {return}
print(location)        }```
打印结果:
<+37.17858340,-122.40641700> +/- 5.00m (speed -1.00 mps / course -1.00) @ 11/11/16, 10:05:05 PM China Standard Time
<+37.17858340(纬度),-122.40641700(经度)> +/- 5.00m(水平精确度) (speed (速度)-1.00 mps / course(航向) -1.00) @ 11/11/16, 10:05:05 PM China Standard Time(获取当前位置的时间)


####应用:指南针
```obj
// 开始获取设备朝向
if CLLocationManager.headingAvailable() {
            locationM.startUpdatingHeading()
        } else {
            print("磁力计损坏,建议更换手机")
        }```
当获取到一个新的设备朝向时就会来到这个方法
```obj
//manager: 位置管理者
///  - newHeading: 新的设备朝向,为负数的话,朝向不可用
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { }

获取磁北角度

let magneticHeading = newHeading.magneticHeading

六、区域监听(CLCircularRegion)

// 主动请求用户的位置对于这个区域的状态
// 一个区域被监听之后,才能去请求这个区域的状态
// 当一个区域能被监听之后(didStartMonitoringFor),会把该区域放入一个集合中monitoredRegions
// 这个集合最多能存储20个区域

七、地理编码(CLGeocoder)

必须联网

地理编码:

.geocodeAddressString("广州", completionHandler: {(clpls : [CLPlacemark]?,error : Error? )in
            print("地理编码成功")
        })```
反地理编码:
```obj
geoc.reverseGeocodeLocation(location, completionHandler: {(clpls : [CLPlacemark]?,error : Error? )in
            print("反地理编码成功")
            })```

地标对象(CLPlacemark):
             location : 用户的位置.(经纬度信息)
             name : 地址
             locality : 城市
             country : 国家

##MapKit
###一、地图基本使用
** 使用前要导入头文件import MapKit** 

* 1.设置地图的类型(mapType)
        >case standard :普通地图(默认)
        case satellite : 卫星云图
        case hybrid : 混合地图(卫星云图的基础上加上普通地图)
        @available(iOS 9.0, *)
        case satelliteFlyover :3D卫星地图 // 做适配
        case hybridFlyover :3D混合卫星地图(3D卫星地图 + 普通地图) // 做适配
>** 如设置为默认:mapView.mapType = .standard **

* 2.设置地图的操作项
         >禁止缩放:mapView.isZoomEnabled = false
         禁止旋转:mapView.isRotateEnabled = false
         禁止拖动:mapView.isScrollEnabled = false

*  3.设置地图的显示项
>iOS 9.0以后有的:
比例尺:mapView.showsScale = true
指南针:mapView.showsCompass = true
显示交通:mapView.showsTraffic = true

 >iOS 9.0之前有的:
显示建筑物:mapView.showsBuildings = true
显示兴趣点: mapView.showsPointsOfInterest = true

```obj
// 显示用户的位置
        // 在iOS8.0之后需要主动请求授权,调用locationM的getter方法
        _ = locationM
        mapView.showsUserLocation = true```
        
 设置用户的追踪模式
        >case none // 不追踪,也不会显示用户的位置(相当于showsUserLocation为false)
        case follow // 追踪,会显示用户的位置showsUserLocation为true
        case followWithHeading // 带放心的追踪 showsUserLocation为true
        // 有一个缺陷,只要动一下地图,就不再追踪用户的位置(不灵光)
>//        mapView.userTrackingMode = .followWithHeading

4.定位、追踪
>步骤:
        1、添加MapView到控制器view上面,并导入头文件import MapKit,可显示标准的默认地图
        2、设置地图的类型(mapType:standard、satellite、hybrid、satelliteFlyover、hybridFlyover)
        3、设置地图的操作项(缩放、旋转、拖动)
        4、设置地图的显示项(比例尺、指南针、交通、建筑物、兴趣点)
        5、显示用户的位置(iOS8.0后需要主动请求定位授权)
        6、设置用户的追踪模式(none、follow、followWithHeading)
        7、设置地图的代理,当用户位置改变,会调用代理方法
        8、在代理方法:①设置大头针标题、子标题;②把用户的位置设置为地图的中心点

代码实现:
```obj
import UIKit
import MapKit

class ViewController: UIViewController {
    @IBOutlet weak var mapView: MKMapView!

    lazy var locationM:CLLocationManager = {
        let locationM = CLLocationManager()
        //请求前后台授权定位
        locationM.requestAlwaysAuthorization()
        return locationM
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
//        1、添加MapView到控制器view上面,并导入头文件import MapKit,可显示标准的默认地图
//        2、设置地图的类型(standard、satellite、hybrid、satelliteFlyover、hybridFlyover)
        mapView.mapType = .standard
        
//        3、设置地图的操作项(缩放、旋转、拖动)--> 默认都开了,所以不设置
//        4、设置地图的显示项(比例尺、指南针、交通、建筑物、兴趣点)
        mapView.showsBuildings = true      //建筑物
        mapView.showsPointsOfInterest = true//兴趣点
        if #available(iOS 9.0, *) {
            mapView.showsScale = true      //比例尺
            mapView.showsCompass = true    //指南针
            mapView.showsTraffic = true    //交通
        }
        
//        5、显示用户的位置(iOS8.0后需要主动请求定位授权)--> 懒加载位置管理者
        _ = locationM  //主动请求授权,配置info.plist文件
        mapView.showsUserLocation = true
        
//        6、设置用户的追踪模式(none、follow、followWithHeading)
        mapView.userTrackingMode = .follow//设置了这个可以不用设置showsUserLocation,区域跨度比设置showsUserLocation更小,显示的用户位置更详细
        
//        7、设置地图的代理,当用户位置改变,会调用代理方法
        mapView.delegate = self
        
//        8、在代理方法:①设置大头针标题、子标题;②把用户的位置设置为地图的中心点
    }
}


//MARK:- mapView的代理方法
extension ViewController:MKMapViewDelegate{
    //当用户位置改变,会调用代理方法
    func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
        //①设置大头针标题、子标题;
        userLocation.title = "我的位置😝"
        userLocation.subtitle = "广州市、天河区、盛达商务园……"
        
        //②把用户的位置设置为地图的中心点
        let coordinate = userLocation.coordinate    //通过用户的位置,获取经纬度
        
        //方法一:
        // 可以实现追踪用户的位置,缺陷:默认情况下不会放大地图的显示区域,需要手动放大,会把之前设置的userTrackingMode为follow变回none
//        mapView.setCenter(coordinate, animated: true)
        //方法二:
        //span区域跨度:在地图上 东西经各180度,总共360度,显示的区域跨度为0-360度之间,南北纬各90度,总共180度,显示的区域跨度为0-180度,设置的越小,那么看到的内容就越清晰
        let span = MKCoordinateSpan(latitudeDelta: 0.00001, longitudeDelta: 0.00001)
        //center : 地图的中心点(经度和纬度)
        let region = MKCoordinateRegion(center: coordinate, span: span)
        //设置地图的显示区域
        mapView.setRegion(region, animated: true)
    }
    
    //当地图显示区域改变时,会调用这个方法
    func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
        print(mapView.region)
    }
}

** 5.大头针对象(MKAnnotation)**
按照MVC的原则
注:在地图上操作大头针,实际上是控制大头针数据模型
1. 添加大头针就是添加大头针数据模型
2. 删除大头针就是删除大头针数据模型
注:不能直接用系统的MKAnnotation,要自定义一个大头针模型,因为系统的能操控大头针属性的全部是只读的

步骤:
1、获取在控件上点击的点
2、将控件上面的点(CGPoint),转为经纬度
3、创建大头针数据模型,并添加到地图上:注:必须先设置title和subTitle的占位字
①通过模型创建大头针;
②确定大头针的经纬度(在地图上显示的位置);
③设置大头针弹框的标题和子标题;
④添加到地图上
4、将点击的那个点的经纬度进行 反地理编码,得到弹框要显示的标题和子标题
① 获取需要反地理编码的经纬度,懒加载地理编码对象:CLGeocoder
②判断反地理编码是否成功
③取出地标对象(CLPlacemark)
④通过地标对象获取城市、详细地址,更新大头针标题和子标题

代码实现:

import UIKit
import MapKit

class ViewController: UIViewController {
    @IBOutlet weak var mapView: MKMapView!
    
    /// 地理编码
    lazy var geoc:CLGeocoder = {
        return CLGeocoder()
    }()
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
//    1、获取在控件上点击的点
        let point = touches.first?.location(in: mapView)
        
//    2、将控件上面的点(CGPoint),转为经纬度
        let coordinate = mapView.convert(point!, toCoordinateFrom: mapView)
        
//    3、创建大头针数据模型,并添加到地图上:注:必须先设置title和subTitle的占位字
        let annotation = addAnnotation(coordinate: coordinate, title: "城市", subTitle: "地址")
//    4、将点击的那个点的经纬度进行 反地理编码,得到弹框要显示的标题和子标题
                //①获取需要反地理编码的经纬度,懒加载地理编码对象:CLGeocoder
        let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
        geoc.reverseGeocodeLocation(location, completionHandler: {
            (clpls:[CLPlacemark]?, error:Error?) -> Swift.Void in
            
                //②判断反地理编码是否成功
            if error != nil{return}
            
                //③取出地标对象(CLPlacemark)
            let clpl = clpls?.first
            
                //④通过地标对象获取城市、详细地址,更新大头针标题和子标题
            annotation.title = clpl?.administrativeArea
            annotation.subtitle = (clpl?.locality)! + (clpl?.subLocality)!
            annotation.subtitle = annotation.subtitle! + (clpl?.name)!
        })
    }
    
    /// 移除大头针
    @IBAction func removeAnnotation(_ sender: UIBarButtonItem) {
        // 1.获取需要移除的大头针
        let annotation = mapView.annotations
        
        // 2.移除大头针
        mapView.removeAnnotations(annotation)
    }
}

//MARK:- 创建大头针
extension ViewController{
    /// 创建大头针,添加到地图上
    /// - Parameters:
    ///  - coordinate: 经纬度
    ///  - title: 标题
    ///  - subTitle: 小标题
    /// - Returns: 大头针
    func addAnnotation(coordinate:CLLocationCoordinate2D,title:String ,subTitle:String) -> JGAnnotation{
        //①通过模型创建大头针;
        let annotation = JGAnnotation()
        
        //②确定大头针的经纬度(在地图上显示的位置);
        annotation.coordinate = coordinate
        
        //③设置大头针弹框的标题和子标题;
        annotation.title = title
        annotation.subtitle = subTitle
        
        //④添加到地图上
        mapView.addAnnotation(annotation)
        
        return annotation
    }
}

** 自定义大头针:**
mapview提供了一个代理方法,当添加一个大头针模型时,系统自动会调用这个代理方法,去设置大头针的视图,具体步骤:

步骤跟创建tableViewCell一样
1、创建标识
2、从缓存池中获取大头针视图
3、如果没有获取到,就创建大头针视图
4、设置大头针数据模型
5、设置大头针样式(弹框、弹框偏移、弹框的左右底部控件、大头针偏移、下降动画、拖拽)

//MARK:- mapview代理方法,自定义大头针
extension ViewController:MKMapViewDelegate{
    
    /// 当添加一个大头针模型时系统自动会调用这个方法,去设置大头针视图
    /// 如果不实现这个方法,则默认用系统的
    /// - Parameters:
    ///  - mapView: 地图视图
    ///  - annotation: 大头针数据模型
    /// - Returns: 返回创建好的大头针视图
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        //步骤跟创建tableViewCell一样
//        1、创建标识
        let ID = "annotationID"
        
//        2、从缓存池中获取大头针视图
        var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: ID)
        
//        3、如果没有获取到,就创建大头针视图
        if annotationView == nil {
            annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: ID)
        }
//        4、设置大头针数据模型
        annotationView?.annotation = annotation
        
//        5、设置大头针样式(弹框、弹框偏移、弹框的左右底部控件、大头针偏移、下降动画、拖拽)
        //如果自定义了大头针视图,必须要指定长什么样
        annotationView?.image = UIImage(named: "category_1")
        //必须设置允许弹框,才会弹框
        annotationView?.canShowCallout = true
        //设置大头针中心偏移量,弹框偏移量
//        annotationView?.centerOffset = CGPoint(x: -50, y: 50)
//        annotationView?.calloutOffset = CGPoint(x: 50, y: 50)
        
        //设置大头针可被拖拽,无法设置下降(animatesDrop)动画了
        annotationView?.isDraggable = true
        
        //设置弹框左右视图,底部视图
        let imageView1 = UIImageView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
        imageView1.image = UIImage(named: "huba")
        annotationView?.leftCalloutAccessoryView = imageView1   //左边视图
        
        let imageView2 = UIImageView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
        imageView2.image = UIImage(named: "htl")
        annotationView?.rightCalloutAccessoryView = imageView2   //左边视图
        
        //底部视图,是iOS9.0之后有的
//        if #available(iOS 9.0, *) {
//            annotationView?.detailCalloutAccessoryView = UISwitch()
//        }
        
        return annotationView
    }
}

二、导航

1、方案一:用系统自带的app导航

步骤:
用系统自带app导航,反推法
1、打开系统导航app进行导航:MKMapItem.openMaps
2、需要2个参数:MKMapItem数组和操作项的字典
3、操作项的字典:驾驶导航、地图类型、是否显示交通,敲MKlaunch会提醒出所有key
4、MKMapItem:起始点、结束点
5、起点、终点需要传地标对象(通过地理编码获得)

import UIKit
import MapKit

class ViewController: UIViewController {
    
    lazy var geoc:CLGeocoder = {
        return CLGeocoder()
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //用系统自带app导航,反推法
//        1、打开系统导航app进行导航:MKMapItem.openMaps
//        2、需要2个参数:MKMapItem数组和操作项的字典
//        3、操作项的字典:方向模式、地图类型、追踪
//        4、MKMapItem:起始点、结束点
//        5、起点、终点需要传地标对象(通过地理编码获得)
        
        //地理编码起点、终点
        geoc.geocodeAddressString("广州", completionHandler: {
            (clpls:[CLPlacemark]?, error:Error?) -> Swift.Void in
            if error != nil{return}
            guard let startCLPL = clpls?.first else{return}
            
            //获取终点的地标
            self.geoc.geocodeAddressString("赣州", completionHandler: { (clpls:[CLPlacemark]?, error:Error?) in
                if error != nil{return}
                guard let endCLPL = clpls?.first else{return}
                
                //打开导航
                self.openToNavigationMap(startCLPL: startCLPL, endCLPL: endCLPL)
            })
        })
        
        
        
    }
    
    func openToNavigationMap(startCLPL:CLPlacemark,endCLPL:CLPlacemark) -> () {
        //设置起点
        let startPlace = MKPlacemark(placemark: startCLPL)
        let startItem:MKMapItem = MKMapItem(placemark: startPlace)
        
        //设置终点
        let endPlace = MKPlacemark(placemark: endCLPL)
        let endItem:MKMapItem = MKMapItem(placemark: endPlace)
        
        //设置
        let items:[MKMapItem] = [startItem,endItem]
        
        //设置地图操作项
        let launchOptions : [String : Any] = [
            MKLaunchOptionsDirectionsModeKey:MKLaunchOptionsDirectionsModeDriving,//驾驶
            MKLaunchOptionsMapTypeKey:MKMapType.hybrid.rawValue,//混合地图
            MKLaunchOptionsShowsTrafficKey:true]//交通可见
        
        //打开系统的导航APP进行导航
        MKMapItem.openMaps(with: items, launchOptions: launchOptions)
    }
}

2、方案二:发送网络请求给苹果服务器

发送网络请求给苹果服务器,获取导航路线:反推法
1、创建路线请求对象
2、设置路线请求的原始点
3、设置路线请求的目的地点
4、创建路线规划对象(MKDirections),去请求路线信息
5、计算路线信息
6、将计算出来的路线的折线覆盖层模型数据 添加到地图,触发代理方法,去设置覆盖层的渲染图层
①根据响应获得路线
②根据路线获得折线数据模型
③将折线数据模型添加到地图(路线也是覆盖层,只要添加了覆盖层就会触发代理方法去渲染图层)
7、在代理方法中画出线路

import UIKit
import MapKit

class ViewController: UIViewController {
    @IBOutlet weak var mapView: MKMapView!
    
    /// 懒加载创建地理编码对象
    lazy var geoc:CLGeocoder = {
        return CLGeocoder()
    }()
    lazy var locationM:CLLocationManager = {
        let locationM = CLLocationManager()
        locationM.requestAlwaysAuthorization()
        return locationM
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        _  = locationM
        mapView.showsUserLocation = true
        mapView.userTrackingMode = .follow
        mapView.delegate = self
        
        //发送网络请求给苹果服务器,获取导航路线:反推法
//        1、创建路线请求对象(MKDirectionsRequest)
//        2、设置路线请求的原始点(地理编码)
//        3、设置路线请求的目的地点(地理编码)
//        4、创建路线规划对象(MKDirections),去请求路线信息
//        5、计算路线信息
//        6、将计算出来的路线的折线覆盖层模型数据 添加到地图,触发代理方法,去设置覆盖层的渲染图层
//        7、在代理方法中画出线路
        
        
        //原始点、目的地点地理编码
        geoc.geocodeAddressString("赣州", completionHandler: {
            (clpls:[CLPlacemark]?, error:Error?) -> Swift.Void in
            if error != nil {return}
            guard let sourceClpl = clpls?.first else{return}
            
            self.geoc.geocodeAddressString("广州", completionHandler: {clpls,error in
                if error != nil {return}
                
                guard let destinationCLPL = clpls?.first else {return}
                
                self.requestDirections(sourceCLPL: sourceClpl, destCLPL: destinationCLPL)
            })
        })
    }
    
    
    func requestDirections(sourceCLPL:CLPlacemark,destCLPL:CLPlacemark) -> () {
        //1、创建路线请求对象(MKDirectionsRequest)
        let directRequest = MKDirectionsRequest()
        
        //2、设置路线请求的原始点(地理编码)
        let sourcePlace = MKPlacemark(placemark: sourceCLPL)
        directRequest.source = MKMapItem(placemark: sourcePlace)
        
        //3、设置路线请求的目的地点(地理编码)
        let destinationPlace = MKPlacemark(placemark: destCLPL)
        directRequest.destination = MKMapItem(placemark: destinationPlace)
        
        //4、创建路线规划对象(MKDirections),去请求路线信息
        let direction = MKDirections(request: directRequest)
        
        //5、计算路线信息
        direction.calculate { (response:MKDirectionsResponse?, error:Error?) in
        //6、将计算出来的路线的折线覆盖层模型数据 添加到地图,触发代理方法,去设置覆盖层的渲染图层
            //①根据响应获得路线
            guard let route =  response?.routes.first else{return}
            
            //②根据路线获得折线数据模型
            let polyline = route.polyline
            
            //③将折线数据模型添加到地图(路线也是覆盖层,只要添加了覆盖层就会触发代理方法去渲染图层)
            self.mapView.add(polyline)
        }
    }
    
    //添加一个圆形覆盖层
    
}

//MARK:- 代理方法中画出线路
extension ViewController:MKMapViewDelegate{
    /// 当往地图上添加覆盖层,就会来到该方法,在此处创建该覆盖层数据模型的渲染图层
    /// 注:不同覆盖层数据模型,使用不同的渲染图层,否则会崩溃
    /// - Parameters:
    ///  - mapView: 地图视图
    ///  - overlay: 覆盖层数据模型
    /// - Returns: 覆盖层渲染图层
    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        
        var  render = MKOverlayPathRenderer()
        
        //注:不同覆盖层数据模型,使用不同的渲染图层,否则会崩溃
        if overlay.isKind(of: MKCircle.self){ //圆形图层
            //1、创建圆形渲染图层
            let circleRenderer = MKCircleRenderer(overlay: overlay)
            
            //2、设置颜色
            circleRenderer.fillColor = UIColor.blue
            circleRenderer.alpha = 0.3
            render = circleRenderer
        }else if overlay.isKind(of: MKPolyline.self) {    //折线图层
            
            //1、创建折线渲染图层
            let polylineRenderer = MKPolylineRenderer(overlay: overlay)
            
            //2、设置线宽、颜色
            polylineRenderer.lineWidth = 5
            polylineRenderer.strokeColor = UIColor.green
            render = polylineRenderer
            
        }
        return render
    }
    
    //当用户位置更新时
    func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
        let coordinate = userLocation.coordinate
        
        //添加圆形覆盖层
        let circle = MKCircle(center: coordinate, radius: 10000)
        mapView.add(circle)
    }
}

3、方案三:百度SDK

这个方法太占内存,不推荐使用,建议了解,后面我封装好了工具类,只需要导入22个静态库就能很轻松使用
** 展示百度地图步骤:
详细步骤在百度的SDK的指南里说的很清楚,可以点进这里来看看:http://lbsyun.baidu.com/index.php?title=iossdk/guide/buildproject**

  • 展示地图:

1、在百度地图开发里申请密钥,下载百度的SDK
2、配置环境:引入mapapi.bundle资源文件时,需要拖BaiduMapAPI_Map.framework,和里面的Resources文件。需要什么功能就拖入什么静态库
3、AppDelegate里面启动BaiduMapManager,关注网络和授权验证

    //1、初始化懒加载mapManager
lazy var mapManager:BMKMapManager = {
        return BMKMapManager()
    }()
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // 1、要使用百度地图,请先启动BaiduMapManager
        _ = mapManager
        // 2、如果要关注网络及授权验证事件,请设定    generalDelegate参数
        let ret = mapManager.start("wejGXHXM8vHPrjU9tL4gXSjqogYpto9H", generalDelegate: self)
        
        if ret == false {
            print("manager start failed!");
        }
        return true
    }

}

//MARK:- BMKGeneral代理
//点进代理去,实现代理里面申明的2个方法
extension AppDelegate:BMKGeneralDelegate{
    /**
     *返回网络错误
     *@param iError 错误号
     */
    func onGetNetworkState(_ iError:Int32){
        if iError == 0 {
            print("网络成功")
        }else{
            print("网络失败")
        }
    }
    /**
     *返回授权验证错误
     *@param iError 错误号 : 为0时验证通过,具体参加BMKPermissionCheckResultCode
     */
    func onGetPermissionState(_ iError:Int32){  //这里要用Int32,不然跟代理方法里面申明的方法不一样了
        if iError == 0 {
            print("授权验证成功")
        }else{
            print("授权验证失败")
        }
    }
  • 4、通过BMKMapView创建地图视图,添加到控制器
    ** 此时能显示地图了 **

** 检索、添加大头针步骤:**

添加头文件:#import <BaiduMapAPI_Search/BMKSearchComponent.h>//引入检索功能所有的头文件
http://lbsyun.baidu.com/index.php?title=iossdk/guide/retrieval
根据网址的文档上的步骤来:
1、创建检索对象
2、发起检索
3、检索成功回调代理方法
4、在代理方法中添加大头针

** 导航、定位 **

1、拖baiduNaviSDK.bundle资源包
2.把AudioToolbox.framework、ImageIO.framework、CoreMotion.framework、CoreLocation.framework、CoreTelephony.framework、MediaPlayer.framework、AVFoundation.framework、SystemConfiguration.framework、JavaScriptCore.framework、Security.framework 、OpenGLES.framework 、GLKit.framework 、libstdc++6.0.9.dylib、libsqlite3.0.tbd、libz.1.2.5.tbd这几个framework添加到工程中
3、“Other Linker Flags”添加“-ObjC” 标识

4、info.plist文件中 设置 “Required background modes”、 “App Transport Security Settings”、 ”NSLocationAlwaysUsageDescription”、 ”NSLocationWhenInUseUsageDescription”、 ”View controller-based status bar appearance”、” LSApplicationQueriesSchemes”
5、添加头文件:

#import“BNCoreServices.h"//导航
#import <BaiduMapAPI_Location/BMKLocationComponent.h>//引入定位功能所有的头文件

** 注:此时编译如果有报错,则改用真机,因为百度的SDK有的版本的静态库不支持模拟机
否则你会一直报这个错误,连编译都不成功**


Snip20161114_5.png

6、在控制器里创建定位服务对象,开始定位,在其处理位置更新的代理方法里进行发起路线规划http://lbsyun.baidu.com/index.php?
7、发起路径规划进行计算路径,当计算成功时会触发回调代理方法,在这个代理方法里开始导航
** 注:如果提示无法TTS授权,则检查这几个原因:1.项目名不能为中文;2.没有(注册)开启语音播报; 3.必须使用真机**
注册语音播报:http://lbsyun.baidu.com/index.php?title=ios-navsdk/guide/voice

封装好的类具体代码实现:

import UIKit

class JGBaiDuMapTool: NSObject {
    /// 提供单例
    static let shareInstance = JGBaiDuMapTool()
    
    /// 懒加载检索对象
    lazy var searcher:BMKPoiSearch = {
        //初始化检索对象
        let searcher = BMKPoiSearch()
        searcher.delegate = self
        return searcher
    }()
    
    /// 创建 定位服务
    lazy var locService:BMKLocationService = {
        let locService = BMKLocationService()
        locService.delegate = self
        return locService
    }()
    
    /// 定义一个闭包,用来回调检索结果
    var poiResultBlock:(([BMKPoiInfo]) -> ())?
    
    
    
    /// 添加大头针
    func addAnnotation(coordinate:CLLocationCoordinate2D,title:String,subtitle:String,mapView:BMKMapView) {
        //创建大头针
        let annotion = BMKPointAnnotation()
        
        //①通过poi列表计算经纬度
        annotion.coordinate = coordinate
        //②通过poi列表拿到title
        annotion.title = title
        //③通过poi列表拿到subtitle
        annotion.subtitle = subtitle
        //④把大头针添加到地图
        mapView.addAnnotation(annotion)
    }
    
    /// 发起检索,触发代理方法,通过闭包在代理方法里面保存到的结果,拿出去给外界用
    func poiSearch(coordinate:CLLocationCoordinate2D,keyword:String,resultBlock:@escaping ([BMKPoiInfo]) -> ()){
        //记录block,用于外界在获取到检索结果时,拿到结果做事情
        self.poiResultBlock = resultBlock
        
        //发起检索
        let option = BMKNearbySearchOption()
        // 起始页
        option.pageIndex = 0
        // 获取多少
        option.pageCapacity = 20
        // 检索的经纬度
        option.location = coordinate
        // 检索的关键字
        option.keyword = keyword
        // 开始检索
        let flag = searcher.poiSearchNear(by: option)
        
        if flag == true{
            print("周边检索发送成功");
        }
        else
        {
            print("周边检索发送失败");
        }
    }
    
    /// 开始导航
    func beginNavigation(coordinate:CLLocationCoordinate2D) {
        //节点数组
        var nodesArray = [BNRoutePlanNode]()
        
        //起点
        let startNode = BNRoutePlanNode()
        startNode.pos = BNPosition()
        startNode.pos.x = coordinate.longitude
        startNode.pos.y = coordinate.latitude
        startNode.pos.eType = BNCoordinate_BaiduMapSDK;
        nodesArray.append(startNode)
        
        //终点
        let endNode = BNRoutePlanNode()
        endNode.pos = BNPosition()
        endNode.pos.x = coordinate.longitude - 1
        endNode.pos.y = coordinate.latitude
        endNode.pos.eType = BNCoordinate_BaiduMapSDK
        nodesArray.append(endNode)
        
        //发起路径规划
        BNCoreServices.routePlanService().startNaviRoutePlan(BNRoutePlanMode_Recommend, naviNodes: nodesArray, time: nil, delegete: self, userInfo: nil)
    }
}

//MARK:- 检索到结果回调
extension JGBaiDuMapTool:BMKPoiSearchDelegate{
    //当检索到结果的时候,会来到这个方法
    func onGetPoiResult(_ searcher: BMKPoiSearch!, result poiResult: BMKPoiResult!, errorCode: BMKSearchErrorCode) {
        if errorCode == BMK_SEARCH_NO_ERROR {
            //在此处理正常结果
            
            guard let poiLists = poiResult.poiInfoList as? [BMKPoiInfo] else{return}        //检索到的结果很多是一个数组
            //将检索获取到的结果,保存在闭包里面
            self.poiResultBlock!(poiLists)
            
        }
        else if errorCode == BMK_SEARCH_AMBIGUOUS_KEYWORD{
            //当在设置城市未找到结果,但在其他城市找到结果时,回调建议检索城市列表
            // result.cityList;
            print("起始点有歧义")
        } else {
            print("抱歉,未找到结果")
        }
    }
}

//MARK:- 导航代理
extension JGBaiDuMapTool:BNNaviRoutePlanDelegate{
    //算路成功回调
    func routePlanDidFinished(_ userInfo: [AnyHashable : Any]!) {
        //路径规划成功,开始导航
        //BNaviUI_NormalNavi,         //正常导航
        //BNaviUI_Declaration,       //声明页面
        BNCoreServices.uiService().showPage(BNaviUI_NormalNavi, delegate: nil, extParams: nil)
    }
    
    
}

//MARK:- 定位代理
extension JGBaiDuMapTool:BMKLocationServiceDelegate{
    //实现相关delegate 处理位置信息更新
    //处理方向变更信息
    func didUpdateUserHeading(_ userLocation: BMKUserLocation!) {
        self.beginNavigation(coordinate: userLocation.location.coordinate)
    }
}

提供github下载链接:
https://github.com/i520junge/JGMap

更多好用的工具类,请持续关注http://www.jianshu.com/users/403e3f9ad3e0/latest_articles

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容