【SwiftUI】MapKitで所要時間と距離数を表示させる方法!時間と分単位に調整
この記事からわかること
- Swift UIのMapKitフレームワークの使い方
- 2地点間の経路の所要時間と距離数を表示させる方法
- 所要時間を時間と分単位に直す方法
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
SwiftUIでMapKitフレームワークを使用して地図上に経路を表示させる方法をまとめていきたいと思います。
MapKitフレームワークの使い方やポイントが分からない方は下記リンクを参考にしてください。
前提と環境
今回はSwiftUIフレームワークを使用している場合に経路の所要時間と距離数を取得し、表示させる方法をまとめていきます。
SwiftUIでは2022年8月時点で経路を表示させる方法がなさそう(私が知らないだけかも?)なのでUiKitで経路ビューを構築してUIViewRepresentable
プロパティを使ってSwiftUIで使用できるようにしています。実装方法は下記記事を参考にしてください。またこの記事も下記記事の続きになっていますのでご注意ください。
注意:SwiftUIとUIKitのMapKitの使い方は少し異なります。今回は経路表示を実装するためにUIKitの場合の地図表示方法で進んでいきますが地図を表示するだけであればSwiftUIのMapView構造体で簡単に表示できますのでこちらの記事をご覧ください。
class MKMapView : UIView // UIKit
public struct Map<Content> : View where Content : View // SwiftUI
所要時間と距離数をSwiftUIで表示する
今回の目標は以下のように所要時間と距離数を上部に表示するビューを作成していきます。
今回の作業の流れとポイント
- ビュー構造体(UIViewRepresentableに準拠)を拡張してCoordinatorクラスを作成
- SwiftUIのビュー・ビュー構造体・Coordinatorクラスの3つにプロパティを作成
- MKRouteのプロパティから所要時間と距離数を取得してプロパティに格納
- ビュー側で表示の整形
Coordinatorクラスの作成
まずはUIViewRepresentable
プロトコルを準拠させた構造体を拡張してCoordinator
クラスを追加します。
struct UIMapView: UIViewRepresentable {
func makeUIView(context: Self.Context) -> MKMapView {
// 省略
}
func updateUIView(_ uiView: MKMapView, context: Self.Context) {
// 省略
}
// makeCoordinatorメソッドを追加
func makeCoordinator() -> Coordinator {
Coordinator(self,expectedTravelTime: $expectedTravelTime,distance: $distance)
}
}
// Coordinatorクラスを拡張して追加
extension UIMapView{
class Coordinator: NSObject {
var control: UIMapView
@Binding var expectedTravelTime:Double // 所要時間 秒単位
@Binding var distance:Double // 距離数 m単位
init(_ control: UIMapView,expectedTravelTime:Binding<Double>,distance:Binding<Double>){
self.control = control
// Binding型はアンダースコア(_)をつける
_expectedTravelTime = expectedTravelTime
_distance = distance
}
}
}
Coordinator
クラスの中には所要時間と距離数を格納するプロパティをビュー構造体と連携できるように@Binding
を使って宣言します。イニシャライザで各プロパティに受け取った値を格納できるようにしておきます。@Binding
を使用したプロパティには名前の先頭に( _:アンダースコア)をつけないとエラーになるので注意してください。
Coordinator
クラスを定義したらインスタンス化するためのmakeCoordinatorメソッド
の定義が必要になります。その際にビュー構造体のプロパティをバインディングして渡します。(この時点ではまだ未定義です)
最初はわざわざCoordinator
クラスを実装しなくても「構造体の中にプロパティを用意すればいけるかも」と思いましたが、makeUIView
メソッドの中でプロパティを更新するかつ(@Stateかmutatingが必須)、SwiftUIのビューのプロパティともバインディングさせたいためこのような形になりました。
各構造体(クラス)にプロパティを準備する
Coordinator
クラスだけではなく、ビュー構造体(UIMapView
)とSwiftUiのビュー(ContentView
)にもプロパティを定義しておきます。
struct UIMapView: UIViewRepresentable {
let Manager = MapManager()
@Binding var expectedTravelTime:Double // 所要時間 秒単位
@Binding var distance: Double // 距離数 m単位
// 以下省略
}
import SwiftUI
import MapKit
struct ContentView: View {
@State var expectedTravelTime:Double = -1 // 所要時間
@State var distance: Double = 0 // 距離数
var body: some View {
UIMapView()
}
}
@State
と@Binding
を使い分けながら宣言しておきます。これで以下のような関係性のプロパティ構造が出来上がります。
大元 ↔︎ 橋渡し ↔︎ 実際に値が格納される
ContentView:expectedTravelTime ↔︎ UIMapView:expectedTravelTime ↔︎ Coordinator:expectedTravelTime
ContentView:distance ↔︎ UIMapView:distance ↔︎ Coordinator:distance
MKRouteのプロパティから所要時間と距離数を取得
2地点間の経路の所要時間と距離数はMKRoute
のプロパティから取得できます。
MKRouteのプロパティ
polyline // 経路を示す線(オブジェクト)
name // ルートの名称
distance // 距離(メートル単位)
expectedTravelTime // 所要時間
func makeUIView(context: Self.Context) -> MKMapView {
// 省略〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
// ルートを取得
let route = directionResonse.routes[0]
// 以下を追加
// 所用時間と距離を格納
context.coordinator.expectedTravelTime = route.expectedTravelTime
context.coordinator.distance = route.distance
// ビューにオーバーレイオブジェクトを追加
mapView.addOverlay(route.polyline, level: .aboveRoads)
定義したCoordinator
クラスのプロパティにはmakeUIView
メソッドの引数で受け取るcontext
のcoordinator
プロパティからアクセスできます。これでプロパティに格納した値が伝播し、ContentView
のプロパティからも同値を参照することができるようになりました。
各プロパティを整形する
取得した値を出力すると以下のように秒単位の時間とメートル単位の距離数になります。ユーザー視点からするとだいぶ不親切なので認識しやすい単位へと変換させる処理を実装していきます。
print(route.expectedTravelTime)
// 結果:20417.0 秒 → 5時間40分(希望)
print(route.distance)
// 結果:507203.0 m → 507.2km(希望)
所要時間を時間と分に直す
秒単位の時間を分に直すには60秒(=1分)で割り算、時間に直すには3600秒(60秒×60分)で割り算すればOKです。
20417.0 秒 ÷ 60秒 = 約340分
20417.0 秒 ÷ ( 60秒 × 60分 ) = 約5.67時間
今回は秒数の時間を受け取った時に所要時間を返すformatTime
メソッドを自作していきます。expectedTravelTimeプロパティの初期値には-1
を与えたので初期値のままなら「経路を検索中...」、0以上の値ならその時間に応じた形式で返すようにswitch
文を使って分岐させました。
func formatTime(_ time:Double) -> String{
switch time {
case -1 :
return "経路を検索中..."
case 0..<60 :
return String(time) + "秒"
case 0..<3600 :
return String(format: "%.0f", time/60) + "分"
default:
let hour = Int(time/3600)
let minutes = (time - Double(hour * 3600))/60
return String(hour) + "時間" + String(format: "%.0f", minutes) + "分"
}
}
小数点以下の値を表示する際はString(format:)
イニシャライザを使用すると便利です。切り上げや切り捨てなどの丸め処理においては以下の記事を参考にしてください。
距離数をkm単位に整形する
距離数に関してはメートル単位で取得できるのでkm
にするために1000m(=1km)で割り算するだけです。
507203.0 m ÷ 1000m = 507.2km
あとはSwift UIのビュー側で表示するだけです。
struct ContentView: View {
@State var expectedTravelTime:Double = -1 // 所要時間
@State var distance: Double = 0 // 距離数
func formatTime(_ time:Double) -> String{
switch time {
case -1 :
return "経路を検索中..."
case 0..<60 : // 1分以下
return String(time) + "秒"
case 0..<3600 : // 1時間以下
return String(format: "%.0f", time/60) + "分"
default: // 1時間以上
let hour = Int(time/3600)
let minutes = (time - Double(hour * 3600))/60
return String(hour) + "時間" + String(format: "%.0f", minutes) + "分"
}
}
var body: some View {
VStack{
HStack{
Spacer()
Image(systemName:"clock")
Spacer()
Text("\(formatTime(expectedTravelTime))").frame(width: 200)
Spacer()
}
HStack{
Spacer()
Image(systemName:"arrow.triangle.turn.up.right.circle")
Spacer()
Text(String(format: "%.1fkm", distance/1000)).frame(width: 200)
Spacer()
}
UIMapView(expectedTravelTime: $expectedTravelTime,distance: $distance)
}
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。
私がSwift UI学習に使用した参考書