この記事について
SwiftUIでクラスタリングに対応したカスタムアノテーションをマップ上に表示しようとしたときに、日本語のドキュメントがあまりなかったので、備忘録として記録しておきます。
※ この記事で扱うのはAppleのマップです
環境
- macOS 12.0.1
- Xcode 13.1
- Swift 5.5.1
実装
SwiftUIにおけるマップの表示
マップを表示する方法は以下の2つ
Mapを利用する方法(iOS 14.0+)
Map
iOS 14 以降であれば、Mapを利用することで簡単にマップを表示することができます.
利用できるアノテーションの種類は
- MapPin
- MapMarker
- MapAnnotation
の3つがあり、MapAnnotation
を利用することでアノテーションをカスタマイズできます.
ただし、現時点で、クラスタリングなどに対応した複雑なカスタムアノテーションを作成するには、MKMapView
を利用するしかないようです.
MKMapViewを利用する方法
MKMapView
今回はこちらを使って実装していきます.
MKMapView
はUIView
なので、SwiftUIで利用するにはUIViewRepresentable
を使って実装する必要があります.
手順
1. CustomAnnotation作成
円形の画像を表示するアノテーションを作成.
クラスタリングされた場合は右上にカウントを表示.
import SwiftUI
struct CustomAnnotation: View {
var count: Int
var imageUrl: String
let imageSize = 70.0
var body: some View {
VStack {
ZStack {
if count > 1 {
// クラスタリングされた場合
Text(String(count)).foregroundColor(.white).frame(width: 30, height: 30).background(Color.main).clipShape(Circle()).zIndex(1).overlay {
Circle().stroke(.white, lineWidth: 1)
}.offset(x: countCoordinate, y: -countCoordinate)
}
AsyncImage(url: URL(string: imageUrl)){ image in
image.resizable().scaledToFill()
} placeholder: {
Rectangle().fill(Color.background)
}.frame(width: imageSize, height: imageSize)
.clipShape(Circle())
.overlay {
Circle().stroke(.white, lineWidth: 3)
}
.shadow(radius: 7)
}
}
}
var countCoordinate: CGFloat {
return imageSize * sqrt(2) / 4 + 2
}
}
2. CustomAnnotationView作成
MKAnnotationViewを継承したクラスを作成.
また、今回はアノテーションに画像を表示するため、MKPointAnnotationを継承した独自のPointAnnotationを用意.
import SwiftUI
import MapKit
class CustomAnnotationView: MKAnnotationView {
let width = 70
let height = 80
var subview: UIView? = nil
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
frame = CGRect(x: 0, y: 0, width: width, height: height)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForDisplay() {
super.prepareForDisplay()
if let sub = subview {
sub.removeFromSuperview()
}
var vc: UIHostingController = UIHostingController(rootView: CustomAnnotation(count: 0, imageUrl: ""))
if let clusterAnnotation = annotation as? MKClusterAnnotation {
let annotations = clusterAnnotation.memberAnnotations
// クラスタされた場合はトップのアノテーションの画像を表示
let firstAnnotation = annotations.first as? CustomPointAnnotation
vc = UIHostingController(rootView: CustomAnnotation(count: clusterAnnotation.memberAnnotations.count, imageUrl: firstAnnotation?.imageUrl ?? ""))
} else {
if let customAnnotation = annotation as? CustomPointAnnotation {
vc = UIHostingController(rootView: CustomAnnotation(count: 0, imageUrl: customAnnotation.imageUrl))
} else {
return
}
}
vc.view.frame = bounds
vc.view.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0)
subview = vc.view
addSubview(vc.view)
}
}
class CustomPointAnnotation: MKPointAnnotation {
var imageUrl: String
init(imageUrl: String) {
self.imageUrl = imageUrl
}
}
3. MapView作成
Map本体.
サンプルとして、アノテーションを2つ追加.
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
@State var centerCoordinate: CLLocationCoordinate2D
var annotations = [MKAnnotation]()
init() {
self.centerCoordinate = CLLocationCoordinate2D(latitude: 35.68154, longitude: 139.752498)
let newLocation = CustomPointAnnotation(imageUrl: "https://picsum.photos/seed/picsum/200/300")
newLocation.coordinate = CLLocationCoordinate2D(latitude: 35.68154, longitude: 139.752498)
annotations.append(newLocation)
let newLocation2 = CustomPointAnnotation(imageUrl: "https://picsum.photos/id/237/200/300")
newLocation2.coordinate = CLLocationCoordinate2D(latitude: 35.700263046992736, longitude: 139.80067894034084)
annotations.append(newLocation2)
}
func makeUIView(context: Context) -> MKMapView {
let view = MKMapView(frame: .zero)
view.pointOfInterestFilter = MKPointOfInterestFilter.excludingAll
view.delegate = context.coordinator
// ここで利用したいAnnotationViewを指定
view.register(CustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
// 今回は、クラスタリングした場合も CustomAnnotationViewを利用する
view.register(CustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier)
view.mapType = .mutedStandard
let region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 35.69, longitude: 139.782498),
span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
)
view.setRegion(region, animated: true)
return view
}
func updateUIView(_ view: MKMapView, context: Context) {
view.addAnnotations(annotations)
}
func makeCoordinator() -> (Coordinator) {
Coordinator(self)
}
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKClusterAnnotation {
let view = mapView.dequeueReusableAnnotationView(withIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier, for: annotation)
// クラスタリングを有効にする場合は指定する.
view.clusteringIdentifier = "cluster"
return view
} else if annotation is CustomPointAnnotation {
let view = mapView.dequeueReusableAnnotationView(withIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier, for: annotation)
view.clusteringIdentifier = "cluster"
return view
}
return nil
}
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
parent.centerCoordinate = mapView.centerCoordinate
}
}
}