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

この記事からわかること
- Swift UIで地図(Maps)を表示する方法
- MapKitフレームワークの使い方
- 地図上をタップ/長押しでピンを設置と住所を表示する方法
- UITapGestureRecognizer/UILongPressGestureRecognizerの使用方法
index
[open]
\ アプリをリリースしました /
SwiftUIにMapKitを導入して地図を表示するアプリの開発中にユーザーからのタップ/長押し時に地図上にピンを設置する機能を実装したので方法を解説していきます。
SwiftUiでMKMapViewを使用する
SwiftUIを使用して地図を表示させる場合はMapView構造体
を使用して地図を表示させますが、2022年8月26日現在ユーザーからのタップなどのイベントを取得して地図上に反映させる方法が見つかりませんでした。
なので仕方なくUIKit
ビューをSwiftUIに使用できるようにするUIViewRepresentable
プロトコルを使ってMKMapView
をSwiftUIで使用できるようにして実装していきます。
流れ
- SwiftUiでMKMapViewを使用するためのビュー構造体を作成
- Coordinatorクラスを追加してイベント時の処理を記述
- 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メソッドのコードの流れ
- タップされた情報を取得(引数:gesture)
- ビューのポイントを取得
- ポイントをマップの座標に変換
- 座標を元にアノテーションを構築
- 定義ずみのアノテーションがあれば削除
- アノテーションをビューに追加
@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
ビューのポイントから地図座標に変換するにはMKMapView
のconvert
メソッドを使用します。引数には変換する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)) { [weak self] placemarks, error in
guard let self else { return }
guard let placemark = placemarks?.first else { return }
// 住所を取得
let administrativeArea = placemark.administrativeArea ?? ""
let locality = placemark.locality ?? ""
let subLocality = placemark.subLocality ?? ""
let thoroughfare = placemark.thoroughfare ?? ""
let subThoroughfare = placemark.subThoroughfare ?? ""
let placeName = !thoroughfare.contains(subLocality) ? subLocality + thoroughfare : thoroughfare
self.address = administrativeArea + locality + placeName + subThoroughfare
print(self.address)
}
mapView.addAnnotation(tapAnotation)
}
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。
私がSwift UI学習に使用した参考書