11
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-03-09

この記事について

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

今回はこちらを使って実装していきます.
MKMapViewUIViewなので、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本体.
サンプルとして、アノテーションを2つ追加.

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

11
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?