[SwiftUI][MapKit] 経路検索してみる

SwiftUI2021

     
⌛️ 2 min.

MapKit で遊んでみます その3 2点間の経路検索をしてみます。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Monterey 13 Beta5
  • Xcode 14.0 Beta5
  • iOS 16.0 beta

MapKit 解説シリーズ記事

MapKit を SwiftUI から使ってみます。

SwiftUI2021 [SwiftUI][MapKit] Map の使い方
SwiftUI2021 [SwiftUI][MapKit] Map に Annotation を表示する方法
SwiftUI2021 [SwiftUI][MapKit] 経路検索してみる

前回までできたことは以下です。
・SwiftUI で Map を使用して地図を表示した
・MKCoordinateRegion を使用して、地図に表示される領域を東京駅周辺に設定した
・位置指定で、Annotation を地図上に表示した

なお、以下では、iOS 上のスクリーンショットを使っていますが、同じコードが macOS 上でも動作します。

MapKit でルート検索する準備

MapKit でルート検索する時に使用するクラス等を前もって説明します。

CLLocation

CoreLocation では、地球上の点を表現するためには、緯度経度をベースとした CLLocationCoordinate2D を使用していました。CLLocationCoordinate2D は、地球上の点を 地球が 球として表現しているので、高度についての情報はありません。

高度情報を付与したものが、CLLocation です。

Apple のドキュメントは、こちら

MKPlacemark/CLPlacemark

地図上の点/位置をデータとして扱う時には、その点に対して 住所等の情報を追加したくなります。
そのようなケースで使用するのが、MKPlacemark/CLPlacemark です。

CLPlacemark についてのドキュメントは、こちら

MKPlacemark についてのドキュメントは、こちら

CLPlacemark は、CoreLocation フレームワークで定義されたクラスで、MKPlacemark は、MapKit で定義されたクラスです。

MKPlacemark は、CLPlacemark を継承したクラスで、MKAnnotationProtocol に準拠しています。

MEMO

CLPlacemark の生成には、CLLocation が必要です。
MKPlacemark の生成には、CLLocationCoordinate2D が必要です。

MKMapItem

Map 上の点情報です。Maps アプリとのやり取りが便利なようにできているクラスです。ルート検索するときに使用します。

Apple のドキュメントは、こちら

MEMO

MKMapItem は生成するためには、MKPlacemark が必要です。

複数の位置を表す型が出てきましたが、ざっくり言うと以下のようになります。
・経路検索で使う位置情報 の型 → MKMapItem
・MKMapItem を生成するのに使う位置情報の型 → MKPlacemark
・MKPlacemark を生成するのに使う位置情報の型 -> CLLocationCoordinate2D

MKRoute

ルート検索の結果として返ってくる型で、1つの経路を表します。
1つの MKRoute がスタート地点からゴール地点への経路情報を(1つ)持っています。

複数経路検索をリクエストすると 複数の MKRoute が返ってきます。

MKRoute からは経路情報として、MKPolyline / [MKRoute.Step] を取得することができます。

MKPolyline は、MKMapPoint で構成される Polyline です。

MKMapPointは、MKMapView に依存した単位で情報を持っていると説明されています。
(SwiftUI の Map を使った時にどうなるかについては説明がありません・・・)

ですが、MKMapPoint の インスタンスメソッドである coordinate を使用することで、地図上の座標として CLLocationCoordinate2D 型の情報を取得することができるので、内部でどのように保持しているかを気にする必要はありません。

MapKitでルート検索してみる

前回までの記事で 特定の位置の地図は表示できたので、MapKit を使って 指定した2点間のルートを計算してみます。

今回は、東京駅~横浜駅を徒歩で歩く前提でルート計算してみます。

ルート検索で使用する主要クラスを順に説明します。

MKDirections

ルート検索での主役は、MKDirections クラスです。
Apple のドキュメントは、こちら

ルート検索は、MKDirections クラス経由で Apple のサーバーにリクエストすることで、実行されます。

MKDirections.Request

リクエストを行う時に使用するクラスです。
スタート地点・ゴール地点や検索条件の詳細を保持します。

Apple のドキュメントは、こちら

source と destination にスタート/ゴール地点の情報をセットします。型は、MKMapItem? です。

calculate/ ルート検索する

以下のような手順で、ルート検索をサーバーに依頼します。

1) 検索条件をセットした MKDirections.Request を生成する
2) request をセットして、MKDirections を生成する
3) directions.calculate を使用してサーバーにルート検索リクエスト
4) 帰ってきた 結果を使用する

       // 1)
       let request = MKDirections.Request()
        request.source = mapItemTokyo
        request.destination = mapItemYokohama
        request.transportType = .walking

        // 2)
        let directions = MKDirections(request: request)
        Task {
            do {
                // 3)
                let result = try await directions.calculate()
                // 4)
                if let route = result.routes.first {
                    routeInfo.addRoutePoints(route.polyline)
                }
            } catch {
                print(error.localizedDescription)
            }
        }

async/await 登場以前の calculate の signature は、以下のようなものでした。

typealias DirectionsHandler = (MKDirections.Response?, Error?) -> Void

func calculate(completionHandler: @escaping MKDirections.DirectionsHandler)

処理が終わると completionHandler が呼び出されると言うものでしたが、async/await を使った 以下のような API が追加されています。

func calculate() async throws -> MKDirections.Response

上記のサンプルのように、await を付与して呼ぶことで、呼び出しが終了するまで待つこともできます。

返ってくる MKDirections.Response は、source:MKMapItem/destination:MKMapItem/routes:[MKRoute] を情報として持っています。routes: [MKRoute] に、ルート検索の結果が入っています。

ルートを表示する

ルート検索で返ってきた値だけで、ルートが想像できる人はいないと思うので、画面上に表示することを考えます。

UIKit/AppKit で用意されている MKMapView であれば、MKPolyline を渡すことで簡単に表示する方法が用意されていますが、SwiftUI の Map 向けには、用意されていません。

前回の記事で説明した MapAnnotation を使って表示してみると以下のようになります。

MapWithRoute

MEMO

なお、東京駅~横浜駅間をルート検索した場合、542点で構成される MKPolyline が返ってきました。
実際に表示する時には、縮尺に応じて間引く等を行った方が良さそうです。

MEMO

ちなみに、MapAnnotation を使って表示する時の重なり具合を調整しようと、zIndex を設定した View を返しても反映されません。渡した順でもないように見えるので、調整不能です。

MEMO

実行中に ”[Font] Failed to parse font key token: hiraginosans-w6” なる warning が表示されます。
この warning は、Map を使っていると、MapAnnotation を使っていない時でも表示されるので、不具合な気がします。とりあえず、無視しました・・・

RoutePoint : 経路上の点を保持するオブジェクト

以下、経路表示の詳細です。

経路上の点(CLLocationCoordinate2D) を保持する enum を作りました。開始・終了点には名称を表示させたいので、必要に応じてその名前も保持できるようにしています。

enum RoutePoint {
    case namedPoint(CLLocationCoordinate2D, String) // String should be unique!
    case point(CLLocationCoordinate2D)

    var name: String {
        switch self {
        case .namedPoint(_ , let name):
            return name
        case .point(_):
            return "No name"
        }
    }

    var location: CLLocationCoordinate2D {
        switch self {
        case .namedPoint(let location,_):
            return location
        case .point(let location):
            return location
        }
    }
}

Identifiable に準拠させておくと、SwiftUI で処理しやすくなるので、以下のような extension を作りました。(点の名称が ユニークであることを前提としています。)

extension RoutePoint: Identifiable {
    var id: String {
        switch self {
        case .namedPoint(_, let string):
            return string
        case .point(let location):
            return String.init(format: "%f", location.latitude * 1000 + location.longitude)
        }
    }
}

この RoutePoint を配列で持つことで、経路を保持することにします。RouteInfo と名前をつけた class を用意しました。この class の変更に応じて 表示を更新させたいので、ObservableObject に準拠させています。

class RouteInfo: ObservableObject {
    @Published var start: RoutePoint
    @Published var end: RoutePoint

    @Published var routePoints: [RoutePoint] = []

    init(_ start:RoutePoint,_ end: RoutePoint) {
        self.start = start
        self.end = end
    }

    func setRoutePoints(_ mkPolyline: MKPolyline) {
        routePoints = [start]
        if mkPolyline.pointCount > 2 {
            for index in 1..(mkPolyline.pointCount-1) {
                let coordinate = mkPolyline.points()[index].coordinate
                routePoints.append(RoutePoint.point(coordinate))
            }
        }
        routePoints.append(end)
    }
}

経路計算が終わったタイミングで、setRoutePoints が呼び出されて、routePoints が設定され、それを契機に、ビューが更新されるという想定です。

開始/終了点は、名称付きで表示したいので、経路検索で返ってきた点のうち、最初と最後の点を置き換えてます。

ここまで作ったところで、準備はできたので、Map を使っている ContentView を以下のように修正しました。

struct ContentView: View {
    @StateObject private var routeInfo = RouteInfo(RoutePoint.namedPoint(tokyoStation, "Tokyo Station"),
                                                   RoutePoint.namedPoint(yokohamaStation, "Yokohama Station"))
    @State private var mapCenter = MKCoordinateRegion(center: tokyoStation.mid(yokohamaStation),
                                                      latitudinalMeters: tokyoStation.distance(yokohamaStation),
                                                      longitudinalMeters: tokyoStation.distance(yokohamaStation))

    var body: some View {
        VStack {
            Text("Tokyo Station")
            Map(coordinateRegion: $mapCenter,
                annotationItems: routeInfo.routePoints, annotationContent: { annotationItem in
                return MapAnnotation(coordinate: annotationItem.location,
                                     anchorPoint: CGPoint(x: 0.5, y: 0.5),
                                     content: {
                    switch annotationItem {
                    case .namedPoint(_, let name):
                        Text(name)
                            .padding(4)
                            .background{ RoundedRectangle(cornerRadius: 5).fill(Color.white.opacity(0.5)) }
                            .overlay{ RoundedRectangle(cornerRadius: 5).stroke(Color.green, lineWidth: 5) }
                            .zIndex(1)
                    case .point(_):
                        Rectangle().fill(Color.red.opacity(0.5)).frame(width:5, height: 5)
                            .zIndex(0.1)
                    }
                })
            })
            Button(action: {
                lookForRoute()
            }, label: {
                Text("Findnig Route!")
            })
        }
        .padding()
    }

    func lookForRoute() {
        // prep MKDirections.Request
        let request = MKDirections.Request()
        request.source = mapItemTokyo
        request.destination = mapItemYokohama
        request.transportType = .walking

        // prep MKDirections
        let directions = MKDirections(request: request)
        Task {
            do {
                let result = try await directions.calculate()
                if let route = result.routes.first {
                    routeInfo.setRoutePoints(route.polyline)
                }
            } catch {
                print(error.localizedDescription)
            }
        }
    }
}

まとめ

MapKit を使った 経路検索を行いました。

MapKit を使った 経路検索
  • 経路検索は、MKDirections 経由で、サーバーにリクエストする
  • 検索する経路情報は、MKDirections.Request を使って設定する
  • 検索された経路は、MKRoute で返ってくる。
  • MKRoute には、MKMapPoint の配列。

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

SwiftUI ViewMastery

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。

英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

SwiftUI 徹底入門

# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です