2
votes

Building a mostly SwiftUI app... I'm trying to connect (bind) the value of a BindableObject in a parent view to a MapKit child view, such that when the annotation (callout accessory) is tapped (the little (i) button on the annotation label...

enter image description here

...it changes the value of $showDetails to true, which is wired up to a "sheet(isPresented: $showDetails...)" further up the view hierarchy, which then displays a modal:

enter image description here

struct MapView: UIViewRepresentable {

    @Binding var mapSelected: Int
    @Binding var showDetails: Bool    // I WANT TO TOGGLE THIS WHEN CALLOUT ACCESSORY IS TAPPED

    @Binding var location: Location
    @Binding var previousLocation: Location

    @Binding var autoZoom: Bool
    @Binding var autoZoomLevel: Int

    class Coordinator: NSObject, MKMapViewDelegate {

        @Binding var showDetails: Bool

        init(showDetails: Binding<Bool>) {
            _showDetails = showDetails
        }

        func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
            guard let coordinates = view.annotation?.coordinate else { return }
            let span = mapView.region.span
            let region = MKCoordinateRegion(center: coordinates, span: span)
            mapView.setRegion(region, animated: true)
        }

        func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
            guard let annotation = annotation as? LocationAnnotation else { return nil }
            let identifier = "Annotation"
            var annotationView: MKMarkerAnnotationView? = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKMarkerAnnotationView
            if annotationView == nil {
                annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
                annotationView?.markerTintColor = UIColor(hex: "#00b4ffff")
                annotationView?.animatesWhenAdded = true
                annotationView?.canShowCallout = true
                annotationView?.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
            } else {
                annotationView?.annotation = annotation
            }
            return annotationView
        }

    func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
            guard let loc = view.annotation as? LocationAnnotation else {
                    print("sorry")
                    return
            }
            print(self.showDetails)     // true, false, true, false, ...
            self.showDetails.toggle()   // THIS DOES TOGGLE, BUT THE VALUE IS NOT OBSERVED BY THE PARENT VIEW
        }
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(showDetails: $showDetails)
    }

    func makeUIView(context: Context) -> MKMapView {
            let map = MKMapView()
            map.delegate = context.coordinator
            return map
    }

    func updateUIView(_ uiView: MKMapView, context: Context) {
            switch mapSelected {
                    case 0:
                            uiView.mapType = .standard
                    default:
                            uiView.mapType = .hybrid
            }
            let currentRegion = uiView.region  // get the current region
            var span: MKCoordinateSpan
            var center: CLLocationCoordinate2D
            var newRegion: MKCoordinateRegion

            if currentRegion.span.latitudeDelta == 90.0 && currentRegion.span.longitudeDelta == 180.0 {   // INITIAL
                    span = MKCoordinateSpan(latitudeDelta: 18.0, longitudeDelta: 18.0)
                    center = CLLocationCoordinate2D(latitude: 54.5, longitude: -110)
                    newRegion = MKCoordinateRegion(center: center, span: span)
                    uiView.setRegion(newRegion, animated: true)
            }
            updateAnnotations(from: uiView)
    }

   // ...
}

What I expect is that when the callout accessory is tapped, showDetails is toggled (which it is), but no effect is seen in the parent view—the sheet is not presented. Seems that the binding is not publishing its new state.

What am I missing / doing wrong? I find integrating UIKit with SwiftUI to be easy, then hard, then easy, then impossible. Help please!

1

1 Answers

2
votes

As it turns out, I was looking in the wrong place. The problem lay not in the above code (which is 100% fine), but rather in a container view that should have been listening to the changed value of showDetails, but wasn't because of the way I passed showDetails into it, e.g.,

ContentView.swift

Footer(search: search,
    locationStore: self.locationStore,
    searchCoordinates: self.searchCoordinates,
    showDetails: self.showDetails) // NOT PASSED AS A BINDING...

Footer.swift

struct Footer: View {

@EnvironmentObject var settingsStore: SettingsStore

@ObservedObject var search: SearchTerm
@ObservedObject var locationStore: LocationStore
@ObservedObject var searchCoordinates: SearchCoordinates

@State var showDetails: Bool   // DECLARE LOCAL STATE, WILL NOT BE AWARE OF CHANGE TO showDetails FROM MapView

Very simple fix:

ContentView.swift

Footer(search: search,
    locationStore: self.locationStore,
    searchCoordinates: self.searchCoordinates,
    showDetails: self.$showDetails)

Footer.swift

struct Footer: View {

@EnvironmentObject var settingsStore: SettingsStore

@ObservedObject var search: SearchTerm
@ObservedObject var locationStore: LocationStore
@ObservedObject var searchCoordinates: SearchCoordinates

@Binding var showDetails: Bool  

It feels like bugs like this one are pretty easy to encounter in SwiftUI (which is awesome, btw) when accidentally using the wrong property wrapper on passed-in variables. The compiler won't warn you and there are no runtime errors, just... nothing happens. And you, like me, may be inclined to chase red herrings a.k.a. perfectly fine code.