I am using SwiftUI to display a map and if user tapped on an annotation, it pops up a detail view in the VStack. I have made the map view and inserted annotations in another SwiftUI file. I also made the detail view.
How can I access the annotations of that map in the main view file to define a .tapaction for them to use it for the detailed view?
I tried defining the view as MKMapView but it is not possible to do it for a UIViewRepresentable inside another SwiftUI view.
The main view (ContentView) code is:
struct ContentView: View {
@State private var chosen = false
var body: some View {
VStack {
MapView()
.edgesIgnoringSafeArea(.top)
.frame(height: chosen ? 600:nil)
.tapAction {
withAnimation{ self.chosen.toggle()}
}
if chosen {
ExtractedView()
}
}
}
}
The MapView code is:
struct MapView : UIViewRepresentable {
@State private var userLocationIsEnabled = false
var locationManager = CLLocationManager()
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
func updateUIView(_ view: MKMapView, context: Context) {
view.showsUserLocation = true
.
.
.
let sampleCoordinates = [
CLLocation(latitude: xx.xxx, longitude: xx.xxx),
CLLocation(latitude: xx.xxx, longitude: xx.xxx),
CLLocation(latitude: xx.xxx, longitude: xx.xxx)
]
addAnnotations(coords: sampleCoordinates, view: view)
}
}
}
I expect to be able to access map view annotations and define tapaction in another view.
In SwiftUI DSL you don't access views.
Instead, you combine "representations" of them to create views.
A pin can be represented by an object - manipulating the pin will also update the map.
This is our pin object:
class MapPin: NSObject, MKAnnotation {
let coordinate: CLLocationCoordinate2D
let title: String?
let subtitle: String?
let action: (() -> Void)?
init(coordinate: CLLocationCoordinate2D,
title: String? = nil,
subtitle: String? = nil,
action: (() -> Void)? = nil) {
self.coordinate = coordinate
self.title = title
self.subtitle = subtitle
self.action = action
}
}
Here's my Map
, which is not just UIViewRepresentable
, but also makes use of a Coordinator
.
(More about UIViewRepresentable
and coordinators can be found in the excellent WWDC 2019 talk - Integrating SwiftUI)
struct Map : UIViewRepresentable {
class Coordinator: NSObject, MKMapViewDelegate {
@Binding var selectedPin: MapPin?
init(selectedPin: Binding<MapPin?>) {
_selectedPin = selectedPin
}
func mapView(_ mapView: MKMapView,
didSelect view: MKAnnotationView) {
guard let pin = view.annotation as? MapPin else {
return
}
pin.action?()
selectedPin = pin
}
func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
guard (view.annotation as? MapPin) != nil else {
return
}
selectedPin = nil
}
}
@Binding var pins: [MapPin]
@Binding var selectedPin: MapPin?
func makeCoordinator() -> Coordinator {
return Coordinator(selectedPin: $selectedPin)
}
func makeUIView(context: Context) -> MKMapView {
let view = MKMapView(frame: .zero)
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: MKMapView, context: Context) {
uiView.removeAnnotations(uiView.annotations)
uiView.addAnnotations(pins)
if let selectedPin = selectedPin {
uiView.selectAnnotation(selectedPin, animated: false)
}
}
}
The idea is:
@State
on the view containing the map, and are passed down as a binding.Coordinator
is the map delegate - I can retrieve the touched MapPin
from the delegate methods.To test it:
struct ContentView: View {
@State var pins: [MapPin] = [
MapPin(coordinate: CLLocationCoordinate2D(latitude: 51.509865,
longitude: -0.118092),
title: "London",
subtitle: "Big Smoke",
action: { print("Hey mate!") } )
]
@State var selectedPin: MapPin?
var body: some View {
NavigationView {
VStack {
Map(pins: $pins, selectedPin: $selectedPin)
.frame(width: 300, height: 300)
if selectedPin != nil {
Text(verbatim: "Welcome to \(selectedPin?.title ?? "???")!")
}
}
}
}
}
...and try zooming/tapping the pin on London, UK :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With