Access EnvironmentObject from nested child

Tried several ways to access parent EnvironmentObject from nested child function, but could not seem to get a method that works. Currently settled on mis-using UserDefaults, but there must be a proper way of doing this. Inside the code, I made comment where access works, and where it doesn’t. Here’s the code:

    struct EditMap: UIViewRepresentable {
        @Environment(\.managedObjectContext) private var viewContext
        @EnvironmentObject var model: ContentModel
        @EnvironmentObject var netStat:NetStatus // internet ok?
        @EnvironmentObject var tgtLink:TargetLink
        @EnvironmentObject var newLink:NewLink
        @State private var myMapView: MKMapView?
        @State var setMapRegion:Bool = true
        @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "timeStamp", ascending: false)])
        var globalLinks: FetchedResults<Link>
        func makeUIView(context: Context) -> MKMapView {
            let mapView = MKMapView()
            mapView.delegate = context.coordinator
            mapView.mapType = .mutedStandard
            mapView.showsUserLocation = false
            let theLink = globalLinks[0]
            // newLink access works ok at this level..
            // so I assume the top level '.environmentObject(NewLink())'
            // sets up the original instance of EnvironmentObject OK.
            let center = CLLocationCoordinate2D(latitude: Double(newLink.lat) ?? theLink.eLat, longitude: Double(newLink.long) ?? theLink.eLong)
            let span = MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2)
            let region = MKCoordinateRegion(center: center, span: span)
            mapView.setRegion(region, animated: false)
            DispatchQueue.global().async {
                self.myMapView = mapView
            }
            let gRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.addAnnotationOnTapGesture(sender:)))
            mapView.addGestureRecognizer(gRecognizer)
            return mapView
        }
        func updateUIView(_ uiView: MKMapView, context: Context) {
            uiView.removeAnnotations(uiView.annotations)
        }
        static func dismantleUIView(_ uiView: MKMapView, coordinator: ()) {
            uiView.removeAnnotations(uiView.annotations)
        }
        // MARK - Coordinator class
        func makeCoordinator() -> Coordinator {
            return Coordinator(map: self)
        }
        class Coordinator: NSObject, MKMapViewDelegate {
            @State private var shouldAnimate = false
            // tried adding @EnvironmentObject var newLink:NewLink here, but no help.
            var newLat:Double = 51.47
            var newLong:Double = 0.0
            var newName:String = ""
            let greenwichCoord = CLLocationCoordinate2D(latitude: 51.447, longitude: 0)
            var map: EditMap
            init(map: EditMap) {
                self.map = map
            }
            func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
                // tried adding @EnvironmentObject var newLink:NewLink here, but also no help.
                var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: Constants.annotationReuseId)
                let markerView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: Constants.annotationReuseId)
                markerView.glyphImage = UIImage(named: "EarthStationRight")
                markerView.markerTintColor = UIColor(Color("darkGreen"))
                markerView.glyphTintColor = UIColor(Color("whiteAndDark"))
                annotationView = markerView
                let newCenter = annotation.coordinate
                let region = MKCoordinateRegion(center: newCenter, span: mapView.region.span )
                mapView.setRegion(region, animated: true)
                newLat = annotation.coordinate.latitude
                UserDefaults.standard.set(fourStr(newLat), forKey: "newLat")
                newLong = annotation.coordinate.longitude
                UserDefaults.standard.set(fourStr(westLong(newLong)), forKey: "newLong")
                let targetLocation = CLLocation(latitude: newLat, longitude: newLong)
                let geoCoder = CLGeocoder()
                DispatchQueue.global().async {
                    geoCoder.reverseGeocodeLocation(targetLocation) { [self] (placemarks, error) in
                        if error == nil && placemarks != nil {
                            let testPlacemark = placemarks?.first
                            let thePlacemark = testPlacemark!
                            let locality = String(thePlacemark.locality ?? "")
                            let area = String(thePlacemark.administrativeArea ?? "")
                            let country = String(thePlacemark.country ?? "")
                            let state = String(thePlacemark.isoCountryCode ?? "")
                            newName = SiteService.locStr(country: country, locality: locality, state: state, area: area)
                            // The following works by using defaults and later retrieving the default values using .onDisappear at top level 'EditMapView'.
                            // But this seems like a mis-use of UserDefaults.
                            UserDefaults.standard.set(newName, forKey: "newName")
                            // There must be some way to access newLink.name from nested Coordinator & mapView levels.
                            // But I always get "Fatal error: No ObservableObject of type NewLink found. A
                            // View.environmentObject(_:) for NewLink may be missing as an ancestor of this view."
                        }
                    }
                }
                return annotationView
            }
            @objc func addAnnotationOnTapGesture(sender: UITapGestureRecognizer) {
                if sender.state == .ended {
                    shouldAnimate = false
                    sender.isEnabled = false
                    let point = sender.location(in: map.myMapView)
                    let coordinate = map.myMapView?.convert(point, toCoordinateFrom: map.myMapView)
                    let annotation = MKPointAnnotation()
                    annotation.coordinate = coordinate ?? greenwichCoord
                    annotation.title = "Select Site"
                    map.myMapView?.removeAnnotations(map.myMapView!.annotations)
                    map.myMapView?.addAnnotation(annotation)
                    // TODO: Wait routine sloppy.  Prevents multiple annotations. Better way?
                    DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
                        sender.isEnabled = true
                    }
                    shouldAnimate = false
                }
            }
        }
    }

UIViewRepresentable conforms to the View protocol, so the environment is available within it. The Coordinator does not, so it can’t access the environment. You would need to use its initializer to pass in anything you need to access from the environment.

Or, and I stress that I haven’t actually tried this, you could try setting a property on your Coordinator in updateUIView using the context.coordinator and context.environment objects. Just a thought.

The init was already there, but I didn’t think about it. Added code as follows and it works!:

                        map.newLink.name = newName
                        map.newLink.lat = fourStr(newLat)
                        map.newLink.long = fourStr(westLong(newLong))

Many thanks for the help.

1 Like