⭐️ はじめに
SwiftUIのMapKitを使ってマップを実装している途中、
病院や飲食店、公園などのあらゆる情報がデフォルトに地図上に表示されていて、
ごちゃごちゃしていると感じました。
 
(なんか汚くないですか?笑)
もし最小対応バージョンが17.0以上なら、
以下のコードで対応できます。
Map(
// 省略
)
.mapStyle(.standard(pointsOfInterest: .excludingAll))
/*
マップにカフェは表示したいなら、以下のコード
.mapStyle(.standard(pointsOfInterest: .including([.cafe])))
*/
しかし、私のプロジェクトの最小対応バージョンは16で、
iOS16ではSwiftUIのMapKitを使ってpointsOfInterestをカスタマイズする方法はなさそうです。
(🙋もし、方法ご存知の方いらっしゃいましたら、コメントお願いします!)
そのため、マップViewはUIViewRepresentableを使ってUIKitで実装し、
SwiftUIのViewで使う方法を採用することにしました。
👨💻 詳細
⭐️UIViewRepresentableとは?
UIViewRepresentableは、SwiftUIでUIKitのUIViewを使うためのプロトコルです。
これを使うことで、UIKitの機能をSwiftUIのViewに統合でき、SwiftUIでは実現が難しい部分を解決できます。
UIViewRepresentableプロトコルに準拠すると、makeUIViewとupdateUIViewという2つのメソッドを必ず実装する必要があります。
⭐️makeUIView(context:)
このメソッドはSwiftUIのViewが初めて表示されるときに一度だけ呼ばれます。
ここではUIKitのView(今回の場合はMKMapView)を初期化や設定します。
 struct CustomMapView: UIViewRepresentable {
    // 省略
    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        
        //これのためにUIViewRepresentableを使いました!
        mapView.pointOfInterestFilter = MKPointOfInterestFilter(including: [.publicTransport])
        mapView.showsUserLocation = true
        mapView.setRegion(region, animated: true)
        
        return mapView
    }
    // 省略
}
⭐️updateUIView(_:context:)
このメソッドはSwiftUI側の状態が更新されるたびに呼び出されます。
@State、@Binding、@ObservedObject、@StateObject などの状態変数が変更されると、SwiftUIがViewを再レンダリングし、updateUIView が呼ばれます。
例えば、ユーザーが地図上で位置を移動し、その新しい位置を基準にデータを取得する場合、SwiftUI側の状態が変わることでupdateUIViewが呼ばれます。
  struct CustomMapView: UIViewRepresentable {
    // 省略
    func updateUIView(_ mapView: MKMapView, context: Context) {
        mapView.removeAnnotations(mapView.annotations)
        
        let annotations = places.map { place in
            let annotation = CustomAnnotation(place: place)
            annotation.coordinate = place.coordinate
            annotation.title = place.displayName
            return annotation
        }
        
        mapView.addAnnotations(annotations)
    }
    // 省略
}
上記の二つのメソッドだけを実装すれば十分な場合もありますが、
私の場合はMKMapViewのdelegateを使う必要があったため、Coordinatorを定義して対応しました。
⭐️Coordinatorとは?
SwiftUIでは、UIKitのdelegateやdataSourceのような仕組みを直接使うことができません。
そのため、UIViewRepresentableではmakeCoordinator()メソッドを使って、SwiftUIとUIKitの橋渡しをするCoordinatorクラスを定義する必要があります
Coordinatorの中でMKMapViewDelegateを実装することで
- ピン(アノテーション)をタップしたときの処理
- 地図が移動されたときの処理
- ピンの見た目をカスタマイズする処理
などのことができます。
⭐️フルコード
struct CustomMapView: UIViewRepresentable {
     func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    func makeUIView(context: Context) -> MKMapView {
        // 省略
        mapView.delegate = context.coordinator // delegateコード追加
        return mapView
    }
    func updateUIView(_ mapView: MKMapView, context: Context) {
        // 省略
    }
    class Coordinator: NSObject, MKMapViewDelegate {
        var parent: CustomMapView
        init(_ parent: CustomMapView) {
            self.parent = parent
        }
        func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
            // ピン(アノテーション)をタップしたときの処理
        }
        func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
            // 地図が移動されたときの処理
        }
        func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
            // ピンの見た目をカスタマイズする処理
        }
    }
}
上記で実装したCustomMapViewを以下のような形でSwiftUIで使用できます!
struct HomeView: View {
    var body: some View {
        CustomMapView()
    }
}
💬 まとめ
SwiftUIはどんどんアップデートされていますが、
企業の事情によりプロジェクトの最小対応バージョンを引き上げるのが難しい場合もあります。
そのため、SwiftUIだけで対応できない部分は
UIViewRepresentableを用いてUIKitの機能を活用することで解決できます。
