SwiftUI Mapを使って一瞬で地図とアノテーションを表示させる
3連休でマトリックス3部作を久々に見切ったションローです。
1作目の公開が1999年だったようなのですが、当時は自分がプログラマーになるとは思っていなかったしちょっと遅刻したくらいで怒られる会社・仕事は嫌だなぁなんて思っていたのを思い出しました。
最近の趣味iOSアプリ開発は基本的にSwiftUIベースでやっているのですが、ある開発で地図表示が必要になったのでその辺りを調べてみました。
SwiftUIでMapを表示する
今回の完全なコードはこちら
SwiftUIを用いて、地図の表示、アノテーションの表示、アノテーションをタップできるようにするまで実装しています。
地図の表示
最低限地図表示に必要なのはMapとMKCoordinateRegionです。
struct MapView: View {
@State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 37.33501993272811, longitude: -122.00912876460102),
latitudinalMeters: 30000, longitudinalMeters: 30000)
var body: some View {
Map(coordinateRegion: $region)
.edgesIgnoringSafeArea(.all)
}
}
regionがBinding<MKCoordinateRegion>になっているのは、ユーザのMapへの操作結果を受け取る必要があるからですね。
これをLive Previewを見ると以下のようになります。
ちなみに .edgesIgnoringSafeArea(.all) はMapをSafeArea内にも表示するために付与しています。
アノテーションを表示する
次はアノテーション表示です。
まずはアノテーションのデータソースになる構造体(今回はAnnotationという名前にしました)を作ります。
struct Annotation: Identifiable {
let id = UUID()
let name: String
let coordinate: CLLocationCoordinate2D
}
この構造体はIdentifiableに準拠している必要があります。
次にこの構造体のリストを作り、Mapに渡します。
struct MapView: View {
@State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 37.33501993272811, longitude: -122.00912876460102),
latitudinalMeters: 30000, longitudinalMeters: 30000)
private var annotations: [Annotation] = [
Annotation(name: "Apple Park",
coordinate: CLLocationCoordinate2D(latitude: 37.33501993272811,
longitude: -122.00912876460102)),
Annotation(name: "Googleplex",
coordinate: CLLocationCoordinate2D(latitude: 37.42231267779884,
longitude: -122.08400387147682)),
Annotation(name: "Facebook HQ",
coordinate: CLLocationCoordinate2D(latitude:37.48493581456912,
longitude: -122.14960893718997)),
]
var body: some View {
Map(coordinateRegion: $region,
annotationItems: annotations,
annotationContent: { (annotation) in
MapAnnotation(coordinate: annotation.coordinate) {
VStack {
Text(annotation.name)
Image(systemName: "mappin.circle.fill")
.foregroundColor(.blue)
}
}
})
.edgesIgnoringSafeArea(.all)
}
}
Mapの引数annotationItemsにAnnotation構造体のリストを渡すのと、annotationContentに「Map上に配置されるアノテーション用のView」を返すクロージャを渡します。
上記のアノテーション用ViewはMapAnnotationProtocolに準拠している必要があり、デフォルトではMapAnnotation<Content>、MapMarker、MapPinが用意されています。
アノテーションの様子は以下です。
若干分かりづらいですが、青い画像 + テキストが3箇所表示されています。
アノテーションをタップできるようにする
最後にタップですが、まずはタップ(選択)されたことをAnnotationに保持できるようにします。
struct Annotation: Identifiable {
let id = UUID()
let name: String
let coordinate: CLLocationCoordinate2D
var selected: Bool = false // <- New!!
}
次にAnnotationのリストに@Stateを付与し、値を変更可能にします。
@State private var annotations: [Annotation] = [
...省略...
最後に、MapAnnotationにジェスチャを追加します。
直接は追加できないので、MapAnnotationが返すViewに付与しましょう。
MapAnnotation(coordinate: annotations.coordinate) {
VStack {
Text(annotation.name)
Image(systemName: "mappin.circle.fill")
.foregroundColor( annotation.selected ? .red : .blue)
.onTapGesture {
if let idx = annotations.firstIndex(where: { $0.id == annotation.id }) {
annotations[idx].selected.toggle()
}
}
}
}
今回は未選択時(青)と選択時(赤)で色を変えるように実装しました。
Live Previewですが、タップ等のインタラクションもできるのマジクソ便利で鼻血が出そうになりますね。🥰
まとめ
SwiftUIでMapを使うとMKMapView + UIViewRepresentableを利用することなく簡単に地図とアノテーション表示、アノテーションとのインタラクションができることを学びました。
ただ、どうもconvertPoint:toCoordinateFromView:に相当するメソッドが存在しないようなので「地図をタップして新規にアノテーションを追加する」を実現する場合はMKMapView + UIViewRepresentableを使う必要があるかもしれません。
もしMapだけで上記を実現する方法を知っている人がいれば教えてもらえると幸いです。
また今回気づいたんですが、noteはgistsを記事内に直接埋め込めるんですね。
フルのコードをgistsに上げてたりするので便利!使っていこう。