2

I'm trying to use @EnvironmentObject to control some aspects of my app. The issue I'm having is that one of my controllers can't access the environment object. I get the fatal error "No @ObservableObject of type Environment found".

I've searched other questions, and every solution I could find consisted of sending .environmentObject(myEnvironment) to the view in question. The problem is this is not a view, and I don't seem to have that option.

Also, in my SceneDelegate I send the environmentObject to the first view, so that is not the problem.

Here is my code.

First, I created a model to declare all my environment variables

Environment

struct Environment { var showMenu: Bool var searchText: String var location : Location init() { self.showMenu = false self.searchText = "" self.location = Location() } } 

Next I have a controller which purpose is to handle any actions related to the environment, right now it has none

EnvironmentController

import Foundation class EnvironmentController : ObservableObject { @Published var environment = Environment() } 

Now, in the SceneDelegate I call the NextDeparturesView, which in turn calls, the MapView.

MapView

import SwiftUI import MapKit //MARK: Map View struct MapView : UIViewRepresentable { @EnvironmentObject var environmentController: EnvironmentController var locationController = LocationController() func makeUIView(context: Context) -> MKMapView { MKMapView(frame: .zero) } func updateUIView(_ uiView: MKMapView, context: Context) { let coordinate = CLLocationCoordinate2D( latitude: environmentController.environment.location.latitude, longitude: environmentController.environment.location.longitude) let span = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1) let region = MKCoordinateRegion(center: coordinate, span: span) uiView.showsUserLocation = true uiView.setRegion(region, animated: true) } } 

You'll notice that in the MapView I call the LocationController, which is where the fatal error occurs

LocationController

import SwiftUI import MapKit import CoreLocation final class LocationController: NSObject, CLLocationManagerDelegate, ObservableObject { //MARK: Vars @EnvironmentObject var environmentController: EnvironmentController @ObservedObject var userSettingsController = UserSettingsController() //var declaration - Irrelevant code to the question //MARK: Location Manager var locationManager = CLLocationManager() //MARK: Init override init() { //more irrelevant code super.init() //Ask for location access self.updateLocation() } //MARK: Functions func updateLocation() { locationManager.delegate = self locationManager.desiredAccuracy = kCLLocationAccuracyBest if locationManager.responds(to: #selector(CLLocationManager.requestAlwaysAuthorization)){ locationManager.requestAlwaysAuthorization() } else { locationManager.startUpdatingLocation() } } //MARK: CLLocationManagerDelegate methods func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { print("Error updating location :%@", error) } func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { switch status { case .notDetermined: self.setDefaultLocation() break case .restricted: self.setDefaultLocation() break case .denied: self.setDefaultLocation() break default: locationManager.startUpdatingLocation() } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { let currentLocation = manager.location?.coordinate self.environmentController.environment.location.latitude = Double(currentLocation!.latitude) self.environmentController.environment.location.longitude = Double(currentLocation!.longitude) manager.stopUpdatingLocation() } //MARK: Other Functions func recenter() { locationManager.startUpdatingLocation() } func setDefaultLocation() { if self.$userSettingsController.userCity.wrappedValue == "" { self.environmentController.environment.location.latitude = 0.0 self.environmentController.environment.location.longitude = 0.0 } else { self.environmentController.environment.location.latitude = self.citiesDictionary[self.userSettingsController.userCity]!.latitude self.environmentController.environment.location.longitude = self.citiesDictionary[self.userSettingsController.userCity]!.longitude } } } 

So, this is where the fatal error occurs. For instance, my app usually calls setDefaultLocation() first, and the app is crashing there. Any idea what I am doing wrong, or how to solve it?

Thank you in advance.

EDIT

After much help from @pawello2222 I've solved my problem, however with some changes to the overall structure of my application.

I will accept his answer as the correct one, but I'll provide a list of things that I did, so anyone seeing this in the future might get nudged in the right direction.

  1. I was wrongly assuming that View and UIViewRepresentable could both access the @EnvironmentObject. Only View can.
  2. In my Environment struct, instead of a Location var, I now have a LocationController, so the same instance is used throughout the application. In my LocationController I now have a @Published var location: Location, so every View has access to the same location.
  3. In structs of the type View I create the @EnvironmentObject var environmentController: EnvironmentController and use the LocationController associated with it. In other class types, I simply have an init method which receives a LocationController, which is sent through the environmentController, for instance, when I call MapView I do: MapView(locController: environmentController.environment.locationController) thus insuring that it is the same controller used throughout the application and the same Location that is being changed. It is important that to use @ObservedObject var locationController: LocationController in classes such as MapView, otherwise changes won't be detected.

Hope this helps.

1

1 Answer 1

2

Don't use @EnvironmentObject in your Controller/ViewModel (in fact anywhere outside a View). If you want to observe changes to Environment in your Controller you can do this:

class Environment: ObservableObject { @Published var showMenu: Bool = false @Published var searchText: String = "" @Published var location : Location = Location() } 
class Controller: ObservableObject { @Published var showMenu: Bool private var environment: Environment private var cancellables = Set<AnyCancellable>() init(environment: Environment) { _showMenu = .init(initialValue: environment.showMenu) environment.$showMenu .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] value in self?.showMenu = value }) .store(in: &cancellables) } } 

You can also use other forms of Dependency Injection to inject the Environment (or even use a singleton).


Generally there are different ways to show your Environment variables (eg. showMenu) in the View (and refresh it):

1) The Environment is injected into your View (NOT to ViewModel) as an @EnvironmentObject - for cases when you need to access the Environment from the View only.

2) The ViewModel subscribes to the Environment (as presented above) and publishes its own variables to the View. No need to use an @EnvironmentObject in your View then.

3) The Environment is injected into your View as an @EnvironmentObject and then is passed to the ViewModel.

Sign up to request clarification or add additional context in comments.

6 Comments

I still can't get it to work. Now it says that the view(MapView) that calls the LocationController, doesn't have the object. Are views not supposed to have access to the environment object if I have it on the parent view? For instance, my entry point view has the object, can't I simply make the call MapView()? Do I have to call it as MapView().environmentObject(envObject)?
Thank you. I'm trying to execute the option number 3. I edited my question to show you what is happening. Hopefully, you can help me.
UIViewRepresentable is not the same as the View. Try passing the Environment to your MapView in init. Something like: MapView(environmentController: environmentController).
I've tried exactly that, but I still can't assign the parameter to the environmentController nor locationController. Must I remove the @EnvironmentObject notation. I've updated the code so you can see what I mean.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.