(SwiftUI) MapKitのルート表示の方法(と修正方法)

SwiftUI で MapKit 上にルートを描画するのは非常に困難だったため、独自の iOS マップ アプリを作成するのに苦労しました。

問題1: overlayの使用方法

問題の詳細

SwiftUI の Map コンポーネントのオーバーレイ メソッドは ShapeStyle に準拠する要素を想定していますが、これを使用して MKPolyline オーバーレイをレンダリングしようとしたため、次のエラーが発生しました: type '() -> ()' cannot conform to 'ShapeStyle'

問題が発生するコード

Map(coordinateRegion: $region, interactionModes: .all, showsUserLocation: true)
    .overlay {
        if let polyline = routePolyline {
            MapPolyline(polyline)
                .stroke(Color.blue, lineWidth: 5)
        }
    }

解決方法

SwiftUI のオーバーレイ メソッドは ShapeStyle 要素を想定しているため、マップ上に MKPolyline を表示するのには適していません。

MKPolyline を正しく表示するには、UIViewRepresentable を使用して MKMapView を SwiftUI に統合し、MKPolylineRenderer を使用してポリラインをレンダリングする必要があります。

この問題の修正後

struct MapView: UIViewRepresentable {
    @Binding var region: MKCoordinateRegion
    @Binding var routePolyline: MKPolyline?

    class Coordinator: NSObject, MKMapViewDelegate {
        var parent: MapView

        init(parent: MapView) {
            self.parent = parent
        }

        func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
            if let polyline = overlay as? MKPolyline {
                let renderer = MKPolylineRenderer(polyline: polyline)
                renderer.strokeColor = .blue
                renderer.lineWidth = 5
                return renderer
            }
            return MKOverlayRenderer(overlay: overlay)
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }

    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
        mapView.setRegion(region, animated: true)
        mapView.showsUserLocation = true
        return mapView
    }

    func updateUIView(_ mapView: MKMapView, context: Context) {
        mapView.setRegion(region, animated: true)
        mapView.removeOverlays(mapView.overlays)
        if let polyline = routePolyline {
            mapView.addOverlay(polyline)
        }
    }
}

問題2: annotationItemsの使用方法

問題の詳細

MKPolyline は注釈 (ピンやマーカー) ではなく、地図上のルート (オーバーレイ) です。ただし、annotationItems注釈のプロパティなので、ここで MKPolyline を使用しようとするとエラーが発生します。また、MKPolyline は Identifiable に準拠していないため、エラーが発生しました。

問題が発生するコード

Map(coordinateRegion: $region, interactionModes: .all, showsUserLocation: true, annotationItems: routePolyline != nil ? [routePolyline!] : []) { polyline in
    MapPolyline(polyline)
        .stroke(Color.blue, lineWidth: 5)
}

解決方法

注釈用のannotationItemsを使用する代わりに、MKPolylineをマップ上のオーバーレイとして扱います。これは、UIViewRepresentableを使用してMKMapViewをラップし、ポリラインをオーバーレイとして追加することで実行できます。

この問題の修正後

struct ContentView: View {
    @State private var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),
        span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
    )

    @State private var routePolyline: MKPolyline?

    var body: some View {
        VStack {
            MapView(region: $region, routePolyline: $routePolyline)
                .frame(height: 400)
                .edgesIgnoringSafeArea(.top)

            Button(action: {
                searchRoute()
            }) {
                Text("Search Route")
            }
            .padding()

            Spacer()
        }
    }

    func searchRoute() {
        let startCoordinate = CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)
        let destinationCoordinate = CLLocationCoordinate2D(latitude: 37.7849, longitude: -122.4294)

        let request = MKDirections.Request()
        request.source = MKMapItem(placemark: MKPlacemark(coordinate: startCoordinate))
        request.destination = MKMapItem(placemark: MKPlacemark(coordinate: destinationCoordinate))
        request.transportType = .automobile

        let directions = MKDirections(request: request)
        directions.calculate { response, error in
            if let error = error {
                print("Route search error: (error.localizedDescription)")
                return
            }

            if let route = response?.routes.first {
                self.routePolyline = route.polyline
                self.region = MKCoordinateRegion(route.polyline.boundingMapRect)
            }
        }
    }
}

問題3: MKPolylineIdentifiable に準拠していません

問題の詳細

(先ほども言いましたが、) MKPolylineIdentifiable に準拠していないため、annotationItems を使用しようとしたときにエラーが発生しました。これは、MKPolyline を注釈として扱うという問題2密接に関連しており、これは正しくありません。

解決方法

Avoid using MKPolyline as an annotation. Instead, handle it properly as an overlay using UIViewRepresentable to integrate MKMapView.

結論

SwiftUI Map コンポーネント基本的なマップ表示に適していますが、MKPolyline などのカスタム オーバーレイのレンダリングはサポートされていません。推奨されるアプローチは、UIViewRepresentable を使用して MKMapView を SwiftUI に統合することです。これにより、高度な MapKit 機能にアクセスでき、マップ機能の安全で柔軟な実装が保証されます。

完全なサンプルコード

import SwiftUI
import MapKit

struct MapView: UIViewRepresentable {
    @Binding var region: MKCoordinateRegion
    @Binding var routePolyline: MKPolyline?

    class Coordinator: NSObject, MKMapViewDelegate {
        var parent: MapView

        init(parent: MapView) {
            self.parent = parent
        }

        func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
            if let polyline = overlay as? MKPolyline {
                let renderer = MKPolylineRenderer(polyline: polyline)
                renderer.strokeColor = .blue
                renderer.lineWidth = 5
                return renderer
            }
            return MKOverlayRenderer(overlay: overlay)
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }

    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
        mapView.setRegion(region, animated: true)
        mapView.showsUserLocation = true
        return mapView
    }

    func updateUIView(_ mapView: MKMapView, context: Context) {
        mapView.setRegion(region, animated: true)

        mapView.removeOverlays(mapView.overlays)

        if let polyline = routePolyline {
            mapView.addOverlay(polyline)
        }
    }
}

struct ContentView: View {
    @State private var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),
        span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
    )
    
    @State private var routePolyline: MKPolyline?

    var body: some View {
        VStack {
            MapView(region: $region, routePolyline: $routePolyline)
                .frame(height: 400)
                .edgesIgnoringSafeArea(.top)

            Button(action: {
                searchRoute()
            }) {
                Text("Search")
            }
            .padding()

            Spacer()
        }
    }

    func searchRoute() {
        let startCoordinate = CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)
        let destinationCoordinate = CLLocationCoordinate2D(latitude: 37.7849, longitude: -122.4294)
        
        let request = MKDirections.Request()
        request.source = MKMapItem(placemark: MKPlacemark(coordinate: startCoordinate))
        request.destination = MKMapItem(placemark: MKPlacemark(coordinate: destinationCoordinate))
        request.transportType = .automobile
        
        let directions = MKDirections(request: request)
        directions.calculate { response, error in
            if let error = error {
                print("Error: (error.localizedDescription)")
                return
            }
            
            if let route = response?.routes.first {
                self.routePolyline = route.polyline
                self.region = MKCoordinateRegion(route.polyline.boundingMapRect)
            }
        }
    }
}

#Preview {
    ContentView()
}

コメント

コメントを残す