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 MKAnnotationView
s. 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
Composing Video and Audio - Video's Audio Is Gone
Swift Spritekit 3D Touch and Touches Moved
Don't Delete Some Rows from Uitableview
Realitykit - How to Add Motion to a Loaded Modelentity from Usdz File
Swift Tuple Has Unexpected Print Result
With Data (Not Nsdata), in Fact How Actually Do You Make a Utf8 Version of a Jpeg
Xcode Firebase | Cannot Convert Value of Type'Authdataresult' to Expected Argument Type 'User'
How to Use the Optional Variable in a Ternary Conditional Operator
Swiftui: Stop an Animation That Repeats Forever
Randomize Two Arrays the Same Way Swift
What the Difference of Keys and Values in Dictionary of Swift
Why Does the Compiler Not See the Default Code in a Protocol
Swiftui How to Programatically Adjust Spacing Between Images and Text
Convert Integer to Roman Numeral String in Swift
How to Take a Substring to the First Index of a Character
Label Showing Top of Screen Instead of Being on the Inputaccessoryview