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.
- I was wrongly assuming that
View
andUIViewRepresentable
could both access the@EnvironmentObject
. OnlyView
can. - In my
Environment
struct, instead of aLocation
var, I now have aLocationController
, so the same instance is used throughout the application. In myLocationController
I now have a@Published var location: Location
, so every View has access to the same location. - In structs of the type
View
I create the@EnvironmentObject var environmentController: EnvironmentController
and use theLocationController
associated with it. In other class types, I simply have aninit
method which receives aLocationController
, which is sent through theenvironmentController
, for instance, when I callMapView
I do:MapView(locController: environmentController.environment.locationController)
thus insuring that it is the same controller used throughout the application and the sameLocation
that is being changed. It is important that to use@ObservedObject var locationController: LocationController
in classes such asMapView
, otherwise changes won't be detected.
Hope this helps.