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"http://導航
#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.lxweimin.com/users/403e3f9ad3e0/latest_articles

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容