【SwiftUI】地図上にタップ/長押しでピンを設置する方法!MapKit

この記事からわかること

  • Swift UI地図(Maps)を表示する方法
  • MapKitフレームワーク使い方
  • 地図上タップ/長押しピン設置住所を表示する方法
  • UITapGestureRecognizer/UILongPressGestureRecognizerの使用方法

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

SwiftUIにMapKitを導入して地図を表示するアプリの開発中にユーザーからのタップ/長押し時に地図上にピンを設置する機能を実装したので方法を解説していきます。

SwiftUiでMKMapViewを使用する

SwiftUIを使用して地図を表示させる場合はMapView構造体を使用して地図を表示させますが、2022年8月26日現在ユーザーからのタップなどのイベントを取得して地図上に反映させる方法が見つかりませんでした

なので仕方なくUIKitビューをSwiftUIに使用できるようにするUIViewRepresentableプロトコルを使ってMKMapViewをSwiftUIで使用できるようにして実装していきます。

流れ

  1. SwiftUiでMKMapViewを使用するためのビュー構造体を作成
  2. Coordinatorクラスを追加してイベント時の処理を記述
  3. UITapGestureRecognizer/UILongPressGestureRecognizerでイベントを取得

注意:SwiftUIとUIKitのMapKitの使い方は少し異なります。今回はタップイベントを実装するためにUIKitの場合の地図表示方法で進んでいきますが地図を表示するだけであればSwiftUIのMapView構造体で簡単に表示できますのでこちらの記事をご覧ください。

class MKMapView : UIView // UIKit
public struct Map<Content> : View where Content : View  // SwiftUI

タップされた場所にピンを設置する方法

地図上をタップされた時にその場所にピンを設置する方法を見ていきます。

まずはUIKitのMkMapViewをSwiftUIでも表示できるようにUIViewRepresentableプロトコルを使用したビュー構造体を定義します。


import SwiftUI
import MapKit
struct UIMapAddressGetView: UIViewRepresentable {
    @State    var mapView = MKMapView()
    
    func makeUIView(context: Self.Context) -> MKMapView {
        return mapView // 後から記述していく
    }
    
    func updateUIView(_ uiView: MKMapView, context: Self.Context) {
    }
}

ここでは特に変わった処理はありません。プロパティにはMKMapViewインスタンスを用意しておきます。

Coordinatorクラスにイベント処理の追加

続いてUIKitのイベントをSwift UIで管理するためのCoordinatorクラスを追加します。プロパティにはビュー構造体のMKMapViewをバインディングできるようにしておきます。タップされた時に実行したい処理はtappedメソッドとして記述していきます。


extension UIMapAddressGetView{
    class Coordinator: NSObject {
        var control: UIMapAddressGetView
        
        @Binding    var mapView:MKMapView
        
        init(_ control: UIMapAddressGetView,mapView:Binding<MKMapView>){
            self.control = control
            _mapView = mapView
        }
        
        @objc  func tapped(gesture: UITapGestureRecognizer) {
            
            let viewPoint = gesture.location(in: mapView)
            let mapCoordinate: CLLocationCoordinate2D = mapView.convert(viewPoint, toCoordinateFrom:mapView)
            let tapAnotation = MKPointAnnotation()
            tapAnotation.coordinate = mapCoordinate
            tapAnotation.title = "ポイント"
            
            if !mapView.annotations.isEmpty{
                mapView.removeAnnotation(mapView.annotations[0])
            }

            mapView.addAnnotation(tapAnotation)
        }
    }
}

タップされた地図上の座標を取得する

tappedメソッドのコードの流れ

  1. タップされた情報を取得(引数:gesture)
  2. ビューのポイントを取得
  3. ポイントをマップの座標に変換
  4. 座標を元にアノテーションを構築
  5. 定義ずみのアノテーションがあれば削除
  6. アノテーションをビューに追加
@objc  func tapped(gesture: UITapGestureRecognizer) {
    
    let viewPoint = gesture.location(in: mapView)
    let mapCoordinate: CLLocationCoordinate2D = mapView.convert(viewPoint, toCoordinateFrom:mapView)
    let tapAnotation = MKPointAnnotation()
    tapAnotation.coordinate = mapCoordinate
    tapAnotation.title = "ポイント"
    
    if !mapView.annotations.isEmpty{
        mapView.removeAnnotation(mapView.annotations[0])
    }
    mapView.addAnnotation(tapAnotation)
}

引数にはUITapGestureRecognizerクラスを指定しておきます。これでタップされた際の情報を引数名gestureで取得できるようになります。

UIGestureRecognizer.location(in view: UIView?)メソッド

func location(in view: UIView?) -> CGPoint

タップされた地図上の座標を取得するにはUIGestureRecognizerクラスのlocation(in:)メソッドを使います。引数に指定したビュー内の場所を返してくれるのでMKMapViewを指定するとMapビューのポイントを返してくれます。

let viewPoint = gesture.location(in: mapView)

MKMapView.convertメソッド

func convert(
    _ point: CGPoint,
    toCoordinateFrom view: UIView?
) -> CLLocationCoordinate2D

ビューのポイントから地図座標に変換するにはMKMapViewconvertメソッドを使用します。引数には変換するCGPoint型の値とビューインスタンスを指定します。

let mapCoordinate: CLLocationCoordinate2D = mapView.convert(viewPoint, toCoordinateFrom:mapView)

MKPointAnnotationクラス

アノテーションはMKPointAnnotationクラスを使って構築していきます。

let tapAnotation = MKPointAnnotation()
    tapAnotation.coordinate = mapCoordinate
    tapAnotation.title = "ポイント"

MKMapView.removeAnnotationメソッド

タップされる度にアノテーションを増やしたくないので追加前にアノテーションをリセットします。アノテーションの有無を識別し、あれば1つしかないはずなので決め打ちで[0]removeAnnotationで削除します。

if !mapView.annotations.isEmpty{
                mapView.removeAnnotation(mapView.annotations[0])
}

最後にアノテーションをビューに追加して終了です。

mapView.addAnnotation(basePin1)

makeCoordinatorの追加

Coordinatorクラスを増やしたのでUIMapAddressGetView構造体にはmakeCoordinatorメソッドを追加しておきます。

func updateUIView(_ uiView: MKMapView, context: Self.Context) {
}
// 追加
func makeCoordinator() -> Coordinator {
    Coordinator(self,mapView:$mapView)
}

UITapGestureRecognizerでタップイベントを追加

続いてタップされた時のイベント処理をビューに追加するためにmakeUIViewメソッドの中からUITapGestureRecognizerを使用してイベント処理を定義し、addGestureRecognizerを使ってmapViewのイベントに追加しておきます。

func makeUIView(context: Self.Context) -> MKMapView {
    let gesture = UITapGestureRecognizer(
        target: context.coordinator,
        action: #selector(Coordinator.tapped)
    )
    mapView.addGestureRecognizer(gesture)
    return mapView
}

これでタップした位置にアノテーションを設置する機能が完了しました。

全体のコードは以下の通りです。

import SwiftUI
import MapKit
struct UIMapAddressGetView: UIViewRepresentable {
    @State    var mapView = MKMapView()
    
    func makeUIView(context: Self.Context) -> MKMapView {
        
        let gesture = UITapGestureRecognizer(
            target: context.coordinator,
            action: #selector(Coordinator.tapped)
        )
        mapView.addGestureRecognizer(gesture)
        return mapView
    }
    
    func updateUIView(_ uiView: MKMapView, context: Self.Context) {
    }
    func makeCoordinator() -> Coordinator {
        Coordinator(self,mapView:$mapView)
    }
}
extension UIMapAddressGetView{
    class Coordinator: NSObject {
        var control: UIMapAddressGetView
        
        @Binding    var mapView:MKMapView
        
        init(_ control: UIMapAddressGetView,mapView:Binding<MKMapView>){
            self.control = control
            _mapView = mapView
        }
        
        @objc  func tapped(gesture: UITapGestureRecognizer) {
            
            let viewPoint = gesture.location(in: mapView)
            let mapCoordinate: CLLocationCoordinate2D = mapView.convert(viewPoint, toCoordinateFrom:mapView)
            let tapAnotation = MKPointAnnotation()
            tapAnotation.coordinate = mapCoordinate
            tapAnotation.title = "ポイント"
            
            if !mapView.annotations.isEmpty{
                mapView.removeAnnotation(mapView.annotations[0])
            }
            // MapViewにピンを追加.
            mapView.addAnnotation(tapAnotation)
        }
    }
}

長押しされた場所にピンを設置する方法

タップの場合だと通常の地図操作の際にもピンが設置されてしまう可能性があるので長押しした場合のみピンを設置するように実装してみます。

長押しの場合は先程のコードのUITapGestureRecognizer部分をUILongPressGestureRecognizerに変えるだけです。今回はメソッド名も変更しておきました。

import SwiftUI
import MapKit
struct UIMapAddressGetView: UIViewRepresentable {
    @State    var mapView = MKMapView()
    
    func makeUIView(context: Self.Context) -> MKMapView {
        
        let gesture = UILongPressGestureRecognizer(
            target: context.coordinator,
            action: #selector(Coordinator.longTapped)
        )
        mapView.addGestureRecognizer(gesture)
        return mapView
    }
    
    func updateUIView(_ uiView: MKMapView, context: Self.Context) {
    }
    func makeCoordinator() -> Coordinator {
        Coordinator(self,mapView:$mapView)
    }
}
extension UIMapAddressGetView{
    class Coordinator: NSObject {
        var control: UIMapAddressGetView
        
        @Binding    var mapView:MKMapView
        
        init(_ control: UIMapAddressGetView,mapView:Binding<MKMapView>){
            self.control = control
            _mapView = mapView
        }
        
        @objc  func longTapped(gesture: UILongPressGestureRecognizer) {
            
            let viewPoint = gesture.location(in: mapView)
            let mapCoordinate: CLLocationCoordinate2D = mapView.convert(viewPoint, toCoordinateFrom:mapView)
            let tapAnotation = MKPointAnnotation()
            tapAnotation.coordinate = mapCoordinate
            tapAnotation.title = "ポイント"
            
            if !mapView.annotations.isEmpty{
                mapView.removeAnnotation(mapView.annotations[0])
            }
            // MapViewにピンを追加.
            mapView.addAnnotation(tapAnotation)
        }
    }
}

長押しされた場所の住所を取得する

ユーザーが長押しした場所の住所を取得できるようにしていきます。ビュー構造体とCoordinatorクラスにaddressプロパティを追加し、長押しされたポイントの住所が格納されるようにしていきます。

import SwiftUI
import MapKit
struct UIMapAddressGetView: UIViewRepresentable {
    @State  var mapView = MKMapView()
    @State  var address:String = ""
    
    func makeUIView(context: Self.Context) -> MKMapView {
        
        let gesture = UILongPressGestureRecognizer(
            target: context.coordinator,
            action: #selector(Coordinator.longTapped)
        )
        mapView.addGestureRecognizer(gesture)
        return mapView
    }
    
    func updateUIView(_ uiView: MKMapView, context: Self.Context) {
    }
    func makeCoordinator() -> Coordinator {
        Coordinator(self,mapView:$mapView,address: $address)
    }
}
extension UIMapAddressGetView{
    class Coordinator: NSObject {
        var control: UIMapAddressGetView
        
        let manager = CLLocationManager()
        let geocoder = CLGeocoder()
        
        @Binding    var mapView:MKMapView
        @Binding    var address:String
        
        init(_ control: UIMapAddressGetView,mapView:Binding<MKMapView>,address:Binding<String>){
            self.control = control
            _mapView = mapView
            _address = address
            
        }
        
        @objc  func longTapped(gesture: UILongPressGestureRecognizer) {
            
            let viewPoint = gesture.location(in: mapView)
            let mapCoordinate: CLLocationCoordinate2D = mapView.convert(viewPoint, toCoordinateFrom:mapView)
            let tapAnotation = MKPointAnnotation()
            tapAnotation.coordinate = mapCoordinate
            tapAnotation.title = "ポイント"
            
            if !mapView.annotations.isEmpty{
                mapView.removeAnnotation(mapView.annotations[0])
            }
            
            geocoder.reverseGeocodeLocation(CLLocation(latitude: mapCoordinate.latitude, longitude: mapCoordinate.longitude)) { placemarks, error in
                if let placemark = placemarks?.first {
                    //住所
                    let administrativeArea = placemark.administrativeArea == nil ? "" : placemark.administrativeArea!
                    let locality = placemark.locality == nil ? "" : placemark.locality!
                    let subLocality = placemark.subLocality == nil ? "" : placemark.subLocality!
                    let thoroughfare = placemark.thoroughfare == nil ? "" : placemark.thoroughfare!
                    let subThoroughfare = placemark.subThoroughfare == nil ? "" : placemark.subThoroughfare!
                    let placeName = !thoroughfare.contains( subLocality ) ? subLocality : thoroughfare
                    self.address = administrativeArea + locality + placeName + subThoroughfare
                    print(self.address)
                }
            }
            mapView.addAnnotation(tapAnotation)
        }
    }
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

私がSwift UI学習に使用した参考書

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index