Swiftui - Mapkit - Binding Mapkit and Show View on Annotation Callout Buttons

SwiftUI - Mapkit - Binding mapkit and show view on annotation callout buttons

import SwiftUI
import MapKit
class Store: Identifiable, ObservableObject {
let id: UUID = UUID()
@Published var title: String?
@Published var latitude: Double
@Published var longitude: Double
@Published var displayStoreImage: Bool
@Published var displayRedMarker: Bool
@Published var redStore: Bool
init(title: String, latitude: Double , longitude: Double, displayStoreImage: Bool, displayRedMarker: Bool, redStore: Bool ) {
self.title = title
self.latitude = latitude
self.longitude = longitude
self.displayRedMarker = displayRedMarker
self.displayStoreImage = displayStoreImage
self.redStore = redStore

}
}
extension Store{
var coordinate: CLLocationCoordinate2D{
get{
return CLLocationCoordinate2D(latitude: self.latitude, longitude: self.longitude)
}
}
}

struct MapViewParent: View {
@State var selectedListing: Store?
@State var results: [Store] = [Store(title: "Point 1 title", latitude: 38.3 , longitude: 123.2, displayStoreImage: true, displayRedMarker: true, redStore: false ), Store(title: "Point 2 title", latitude: 38.4 , longitude: 123.1, displayStoreImage: true, displayRedMarker: false, redStore: true )
]
var body: some View {
VStack{
Button("clear-storepage", action: {
selectedListing = nil
})
if selectedListing != nil{
TopMapMenu(results: $results, selectedListing: $selectedListing)
}else{
MapViewAnnotations(selectedListing: $selectedListing, results: $results)
}
}
}
}
struct MapViewAnnotations: View {
@State var region: MKCoordinateRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 38.3 , longitude: 123.2 ), span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))

@Binding var selectedListing: Store?
@Binding var results: [Store]

var body: some View {
Map(coordinateRegion: $region, annotationItems: results, annotationContent: {
listing in
MapAnnotation(coordinate: listing.coordinate, content: {
Button(action: {
self.selectedListing = listing as Store?
self.region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: listing.coordinate.latitude , longitude: listing.coordinate.longitude ), span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
}, label: {
if listing.displayStoreImage{
if listing.displayRedMarker{
Image(systemName: "mappin.circle.fill").foregroundColor(.red).contentShape(Circle())
}else{
Image(systemName: "mappin").foregroundColor(.green).contentShape(Circle())
}
}
else{
Image(systemName: "mappin.circle.fill").foregroundColor(.red).contentShape(Circle())
}
}).buttonStyle(PlainButtonStyle())
})
})

}
}

struct TopMapMenu: View {
@Binding var results: [Store]
@Binding var selectedListing: Store?
var body: some View {
List{
ForEach(results, id: \.id){store in
StoreView(store: store, selectedListing: $selectedListing, results: $results)
}
}
}
}
struct StoreView: View {
@ObservedObject var store: Store
@Binding var selectedListing: Store?
@Binding var results: [Store]
var body: some View{
HStack{
VStack(alignment: .leading, spacing: 10){
Button(action: {
store.displayRedMarker.toggle()
store.redStore = store.displayRedMarker
}, label: {
HStack {
if store.redStore {
Image(systemName: "heart.fill")
}
else { Image(systemName: "heart") }

Text("Restaurant")
}
Spacer()
.background(Color(.white))
}
)}
MapViewAnnotations(selectedListing: $selectedListing, results: Binding(get: {
return [store]
}, set: {
let idx = results.firstIndex(where: {
store.id == $0.id
})
if idx != nil && $0.first != nil{
results[idx!] = $0.first!
}
}))
}
}
}
struct MapViewParent_Previews: PreviewProvider {
static var previews: some View {
MapViewParent()
}
}

Mapkit Callout Accessory Button Activation

I have found an appropriate workaround to this issue:

(void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view {
[view.superview bringSubviewToFront:view];
}

https://github.com/danielbarela/ios-map-callout-test/issues/1

Display overlay view when selecting annotation in SwiftUI

You want to know the selected annotation in your SwiftUI view. So you have to store it somewhere. Declare a @State :

struct ContentView: View {
let locations: [MKAnnotation]
@State private var selectedLocation: MKAnnotation?
var body: some View {
// ... //
}
}

Now in your wrapper (UIViewRepresentable) you have to make a binding with this MKAnnotation? :

struct MapView: UIViewRepresentable {
@Binding var selectedLocation: MKAnnotation? // HERE
let annotations: [MKAnnotation]

func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.region = // .... //
mapView.addAnnotations(annotations)
mapView.delegate = context.coordinator
return mapView
}

func updateUIView(_ view: MKMapView, context: Context) {
// .... //
}

Now you should be able to access this variable in your Delegate (Coordinator). For that you have to pass the UIViewRepresentable to the Coordinator:

    func makeCoordinator() -> Coordinator {
Coordinator(self)
}

class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView

init(_ parent: MapView) {
self.parent = parent
}

And finally in func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) you can copy the MKAnnotation in parent.selectedLocation .
With the @Binding this MKAnnotation is now accessible in your parent view (ContentView). You can display its properties in your DetailView.

        func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
// ... //
}

func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
parent.selectedLocation = view.annotation
}
}
}

For example :

struct ContentView: View {
let locations: [MKAnnotation]
@State private var selectedLocation: MKAnnotation?
var body: some View {
VStack {
Text("\(selectedLocation?.coordinate.latitude ?? 0)")

// Don't forget the Binding : $selectedLocation

MapView(selectedLocation: $selectedLocation, annotations: locations)
}
}
}

Mapkit with multi annotation (callout), mapping the next view

When you create the UIButton for the annotation, set the tag property (tag is an NSInteger property of UIView) to an id or array index that identifies the relevant object. You can then retrieve that tag value from the sender parameter to your selector.


Edit: here's some sample code.

You create your annotation view and associate the button in your delegate's -mapView:viewForAnnotation: method:

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation {
// Boilerplate pin annotation code
MKPinAnnotationView *pin = (MKPinAnnotationView *) [self.map dequeueReusableAnnotationViewWithIdentifier: @"restMap"];
if (pin == nil) {
pin = [[[MKPinAnnotationView alloc] initWithAnnotation: annotation reuseIdentifier: @"restMap"] autorelease];
} else {
pin.annotation = annotation;
}
pin.pinColor = MKPinAnnotationColorRed
pin.canShowCallout = YES;
pin.animatesDrop = NO;

// now we'll add the right callout button
UIButton *detailButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];

// customize this line to fit the structure of your code. basically
// you just need to find an integer value that matches your object in some way:
// its index in your array of MKAnnotation items, or an id of some sort, etc
//
// here I'll assume you have an annotation array that is a property of the current
// class and we just want to store the index of this annotation.
NSInteger annotationValue = [self.annotations indexOfObject:annotation];

// set the tag property of the button to the index
detailButton.tag = annotationValue;

// tell the button what to do when it gets touched
[detailButton addTarget:self action:@selector(showDetailView:) forControlEvents:UIControlEventTouchUpInside];

pin.rightCalloutAccessoryView = detailButton;
return pin;

}

Then in your action method, you'll unpack the value from tag and use it to display the right detail:

-(IBAction)showDetailView:(UIView*)sender {
// get the tag value from the sender
NSInteger selectedIndex = sender.tag;
MyAnnotationObject *selectedObject = [self.annotations objectAtIndex:selectedIndex];

// now you know which detail view you want to show; the code that follows
// depends on the structure of your app, but probably looks like:
MyDetailViewController *detailView = [[MyDetailViewController alloc] initWithNibName...];
detailView.detailObject = selectedObject;

[[self navigationController] pushViewController:detailView animated:YES];
[detailView release];
}

CallOutAccessory button image state

You can handle this in your viewForAnnotation method on the map. Assuming you have subclassed MKAnnotation and are using reusable MKAnnotationViews. In your annotation class add a flag to determine whether the annotation is selected or not and keep it updated. Then, in your viewForAnnotation method...

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let annotationView = yourMap.dequeueReusableAnnotationView(withIdentifier: "someIdentifier") as! YourCustomAnnotationClass
if annotation.isSelected {
// Setup your button here
}
return annotationView
}

Basically, if you are using reusable annotation views, you have to set up the view similar to how collection views work. Every time viewFor annotation is called this code will run so some weird stuff can happen if you don't manually set it up properly every time or otherwise aren't careful with how the cell is reused.

SwiftUI: Saving MapKit Annotation Data to Pass To Another View

Here is fixed part

class Coordinator: NSObject, MKMapViewDelegate {

var contentData: ContentData // << here !!
var control: MapUIView

init(_ control: MapUIView) {
self.control = control
self.contentData = control.contentData // << here !!
}

// ... other code
}

Note: @ObservedObject is designed to be used in SwiftUI view and not needed in class

How to reset a SwiftUI MapKit coordinate region's center when navigating to a NavigationLink?

When you pass a Binding in to an init, if you need to use the values contained in the Binding, you must use .wrappedValue to access the underlying values. In this case, you are passing in two Binding<Double> and are attempting to use them to create a CLLocationCoordinate2D, so the initializer would have to look like this:

init(latitude:Binding<Double>, longitude:Binding<Double>) {
// Binding
_latitude = latitude
// Binding
_longitude = longitude
_region = State(initialValue :MKCoordinateRegion(
// not Bindings
center: CLLocationCoordinate2D(latitude: latitude.wrappedValue, longitude: longitude.wrappedValue),
span: self.span
))
}


Related Topics



Leave a reply



Submit