【SwiftUI】クラスタリングに対応したカスタムアノテーションをマップ上に表示する

## この記事について

SwiftUIでクラスタリングに対応したカスタムアノテーションをマップ上に表示しようとしたときに、日本語のドキュメントがあまりなかったので、備忘録として記録しておきます。

※ この記事で扱うのはAppleのマップです

### 環境
- macOS 12.0.1
- Xcode 13.1
- Swift 5.5.1

## 実装

### SwiftUIにおけるマップの表示
マップを表示する方法は以下の２つ

#### Mapを利用する方法(iOS 14.0+)
**Map**

https://developer.apple.com/documentation/mapkit/map

iOS 14 以降であれば、Mapを利用することで簡単にマップを表示することができます.
利用できるアノテーションの種類は
- MapPin
- MapMarker
- MapAnnotation

の３つがあり、`MapAnnotation` を利用することでアノテーションをカスタマイズできます.

ただし、現時点で、クラスタリングなどに対応した複雑なカスタムアノテーションを作成するには、`MKMapView`を利用するしかないようです.

https://developer.apple.com/forums/thread/684811

#### MKMapViewを利用する方法
**MKMapView**

https://developer.apple.com/documentation/mapkit/mkmapview

今回はこちらを使って実装していきます.
`MKMapView`は`UIView`なので、SwiftUIで利用するには`UIViewRepresentable`を使って実装する必要があります.

### 手順
#### 1. CustomAnnotation作成
円形の画像を表示するアノテーションを作成.
クラスタリングされた場合は右上にカウントを表示.

```CustomAnnotation.swift
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を用意.

```CustomAnnotationView.swift
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本体.
サンプルとして、アノテーションを２つ追加.

```MapView.swift
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
        }
    }
}

```

## 完成図
![apotApp (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237025/3b399ede-4e64-943f-6d3d-7eb75f18ce76.gif)