Cannot get #EnvironmentObject in Controller - ios

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 and UIViewRepresentable could both access the #EnvironmentObject. Only View can.
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.
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.

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.

Related

Observable property assigned in SceneDelegate turns to nil

In my SceneDelegate I assign the variable of UserInfo() token:
SceneDelegate.swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
#ObservedObject var userInfo = UserInfo()
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
InstanceID.instanceID().instanceID { (result, error) in
if let error = error {
print("ERROR fetching remote instance ID: \(error)")
} else if let result = result {
print("REMOTE instance ID token: \(result.token)")
self.userInfo.token = result.token
}
}
Model.swift
class UserInfo: ObservableObject {
#Published var token: String? = nil{
didSet(newValue){
print("NEW value:\(newValue)")
}
}
}
print("NEW value:\(newValue)") then successfully prints the new value. However, when I access token in another model, it is nil:
class Search: ObservableObject {
let db = Firestore.firestore()
lazy var functions = Functions.functions()
let settings = UserSettings()
#ObservedObject var userInfo = UserInfo()
#ObservedObject var locationManager = LocationManager()
#Published var status = Status.offline
func getUserData() -> [String:Any?]? {
guard let birthday = UserDefaults.standard.string(forKey: "birthday"), let latitude = locationManager.location?.coordinate.latitude, let longitude = locationManager.location?.coordinate.longitude else {
print("BDAY \(UserDefaults.standard.string(forKey: "birthday"))")
print("LOCATION \(locationManager.location?.coordinate.latitude)")
print("TOKEN \(self.userInfo.token)") // prints nil
return nil
}
guard let fcmToken = self.userInfo.token else {
return nil
}
Why is this? userInfo.token is only assigned once at app startup in SceneDelegate - so I'm not sure why it's value changes to nil. Unless there are multiple instances of UserInfo() - however I thought ObservableObject would make it "one source of truth'?
1st: #ObservedObject has sense only within SwiftUI View, so just remove that wrapper from your classes.
2nd: Your SceneDelegate and Search create different instances of UserInfo, so it is obvious that changing one of them does not affect anyhow another one.
Solution: it is not clear from provided snapshot how Search can refer to SceneDelegate, but you have to inject SceneDelegate.userInfo into Search somehow in place of creation latter (either by constructor argument or by assigning property)

Request User Location Permission In SwiftUI

How do you get user location permissions in SwiftUI?
I tried asking for user location permissions after a button tap, but the dialogue box disappears after about a second. Even if you do end up clicking it in time, permission is still denied.
import CoreLocation
.
.
.
Button(action: {
let locationManager = CLLocationManager()
locationManager.requestAlwaysAuthorization()
locationManager.requestWhenInUseAuthorization()
}) {
Image("button_image")
}
Things like location manager should be in your model, not your view.
You can then invoke a function on your model to request location permission.
The problem with what you are doing now is that your CLLocationManager gets released as soon as the closure is done. The permission request methods execute asynchronously so the closure ends very quickly.
When the location manager instance is released the permission dialog disappears.
A location model could look something like this:
class LocationModel: NSObject, ObservableObject {
private let locationManager = CLLocationManager()
#Published var authorisationStatus: CLAuthorizationStatus = .notDetermined
override init() {
super.init()
self.locationManager.delegate = self
}
public func requestAuthorisation(always: Bool = false) {
if always {
self.locationManager.requestAlwaysAuthorization()
} else {
self.locationManager.requestWhenInUseAuthorization()
}
}
}
extension LocationModel: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
self.authorisationStatus = status
}
}
You would probably also want functions to start & stop location updates and an #Published CLLocation property

How can I wait until the LocationService Callback is executed?

I'm trying to execute a Location Service once. This LocationService is called from another object class, that will add the location info into the parameters. All of this will be one object.
The problem is that when I init the object, everything is populated less the location data, which will be populated a few ms later.
I need to wait until the callback is executed, to successfully generate the full object before using it
So considering that I have the next "LocationService" class
public class LocationService: NSObject, CLLocationManagerDelegate{
let manager = CLLocationManager()
var locationCallback: ((CLLocation?) -> Void)!
var locationServicesEnabled = false
var didFailWithError: Error?
public func run(callback: #escaping (CLLocation?) -> Void) {
locationCallback = callback
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
manager.requestWhenInUseAuthorization()
locationServicesEnabled = CLLocationManager.locationServicesEnabled()
if locationServicesEnabled {
manager.startUpdatingLocation()
}else {
locationCallback(nil)
}
}
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
locationCallback(locations.last!)
manager.stopUpdatingLocation()
}
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
didFailWithError = error
locationCallback(nil)
manager.stopUpdatingLocation()
}
deinit {
manager.stopUpdatingLocation()
}
}
And it is called from the object class like this:
class ObjectX: NSObject{
//Class variables
#objc override init() {
let getLocation = LocationService()
getLocation.run {
if let location = $0 {
//get location parameters
}}
Finally the ObjectX class is initiated and used from other place
let getLocation = ObjectX()
//After initiate it I use the object for other purposes, but here the object is not complete, the location parameters have not been populated yet
How can I wait in the class that is calling it until the callback is executed? Should I use getLocation.performSelector()? How?
Maybe this is not the best way to resolve this but it worked for me.
Basically, instead of creating the ObjectX and setting the in the process, the location service is gonna be called the before, then in the callback, the ObjectX is gonna be initialised, then we can set the location parameters for the ObjectX with the location object the we received in the object.
We remove the location set from the initialiser
class ObjectX: NSObject{
//Class variables
#objc override init() {
//Setting the rest of the parameters that are not location
}}
Then class that was initialising the object, we init and run the LocationService, then in callback we create the ObjectX and we set the location parameters
let ls = LocationService()
ls.run { location in
let objectX = ObjectX()
objectX.location = location
//We can use the object here
}

Keep reference on view/data model after View update

Consider we have a RootView and a DetailView. DetailView has it's own BindableObject, let's call it DetailViewModel and we have scenario:
RootView may be updated by some kind of global event e.g. missed
internet connection or by it's own data/view model
When RootView handling event it's
content is updated and this is causes new struct of DetailView to
be created
If DetailViewModel is created by DetailView on init,
there would be another reference of DetailViewModel and it's state (e.g. selected object) will be missed
How can we avoid this situation?
Store all ViewModels as EnvironmentObjects that is basically a singleton pool. This approach is causes to store unneeded objects in memory when they are not used
Pass throw all ViewModels from RootView to it's children and to children of child (has cons as above + painfull dependencies)
Store View independent DataObjects (aka workers) as EnvironmentObjects. In that case where do we store view dependent states that corresponds to Model? If we store it in View it will end up in situation where we cross-changing #States what is forbidden by SwiftUI
Better approach?
Sorry me for not providing any code. This question is on architecture concept of Swift UI where we trying to combine declarative structs and reference objects with data.
For now I don't see da way to keep references that corresponds to appropriate view only and don't keep them in memory/environment forever in their current states.
Update:
Lets add some code to see whats happening if VM is created by it's View
import SwiftUI
import Combine
let trigger = Timer.publish(every: 2.0, on: .main, in: .default)
struct ContentView: View {
#State var state: Date = Date()
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: ContentDetailView(), label: {
Text("Navigation push")
.padding()
.background(Color.orange)
})
Text("\(state)")
.padding()
.background(Color.green)
ContentDetailView()
}
}
.onAppear {
_ = trigger.connect()
}
.onReceive(trigger) { (date) in
self.state = date
}
}
}
struct ContentDetailView: View {
#ObservedObject var viewModel = ContentDetailViewModel()
#State var once = false
var body: some View {
let vmdesc = "View model uuid:\n\(viewModel.uuid)"
print("State of once: \(once)")
print(vmdesc)
return Text(vmdesc)
.multilineTextAlignment(.center)
.padding()
.background(Color.blue)
.onAppear {
self.once = true
}
}
}
class ContentDetailViewModel: ObservableObject, Identifiable {
let uuid = UUID()
}
Update 2:
It seems that if we store ObservableObject as #State in view (not as ObservedObject) View keeps reference on VM
#State var viewModel = ContentDetailViewModel()
Any negative effects? Can we use it like this?
Update 3:
It seems that if ViewModel kept in View's #State:
and ViewModel is retained by closure with strong reference - deinit will never be executed -> memory leak
and ViewModel is retained by closure with weak reference - deinit invokes every time on view update, all subs will be reseted, but properties will be the same
Mehhh...
Update 4:
This approach also allows you to keep strong references in bindings closures
import Foundation
import Combine
import SwiftUI
/**
static func instanceInView() -> UIViewController {
let vm = ContentViewModel()
let vc = UIHostingController(rootView: ContentView(viewModel: vm))
vm.bind(uiViewController: vc)
return vc
}
*/
public protocol ViewModelProtocol: class {
static func instanceInView() -> UIViewController
var bindings: Set<AnyCancellable> { get set }
func onAppear()
func onDisappear()
}
extension ViewModelProtocol {
func bind(uiViewController: UIViewController) {
uiViewController.publisher(for: \.parent)
.sink(receiveValue: { [weak self] (parent) in
if parent == nil {
self?.bindings.cancel()
}
})
.store(in: &bindings)
}
}
struct ModelView<ViewModel: ViewModelProtocol>: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<ModelView>) -> UIViewController {
return ViewModel.instanceInView()
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<ModelView>) {
//
}
}
struct RootView: View {
var body: some View {
ModelView<ParkingViewModel>()
.edgesIgnoringSafeArea(.vertical)
}
}
Apple says that we should use ObservableObject for the data that lives outside of SwiftUI. It means you have to manage your data source yourself.
It looks like a single state container fits best for SwiftUI architecture.
typealias Reducer<State, Action> = (inout State, Action) -> Void
final class Store<State, Action>: ObservableObject {
#Published private(set) var state: State
private let reducer: Reducer<State, Action>
init(initialState: State, reducer: #escaping Reducer<State, Action>) {
self.state = initialState
self.reducer = reducer
}
func send(_ action: Action) {
reducer(&state, action)
}
}
You can pass the instance of the store into the environment of your SwiftUI app and it will be available in all views and will store your app state without data losses.
I wrote a blog post about this approach, take a look at it for more information
https://swiftwithmajid.com/2019/09/18/redux-like-state-container-in-swiftui/

Is it bad idea to create static stream in reactive programming?

Now I'm working on iOS using RxSwift framework. In my app I have to user user location, but I don't need it to be updated in real time. It's enough if location updated every time user opens app or does some defined action. Therefore, how about implementing singleton class where the last result is cached. Each update by action changes cached result and accepts it to the stream. Stream's default value is cached value. Then, views where user location is needed would subscribe on this stream.
Example implementation using Cache and RxSwift
import Foundation
import Cache
import CoreLocation
import RxSwift
import RxCocoa
class UserLocationManager: NSObject {
private enum Keys: String {
case diskConfig = "Disk Config"
case lastLocation = "User Last Location"
}
// MARK: - Variables
private func cache<T: Codable>(model: T.Type) -> Cache.Storage<T> {
let storage = try! Cache.Storage(diskConfig: DiskConfig(name: Keys.diskConfig.rawValue), memoryConfig: MemoryConfig(expiry: .never), transformer: TransformerFactory.forCodable(ofType: model))
return storage
}
private let locationManager = CLLocationManager()
private var lastPosition: MapLocation? {
get {
do {
return try cache(model: MapLocation.self).object(forKey: Keys.lastLocation.rawValue)
}
catch { return nil }
}
set {
do {
guard let location = newValue else { return }
try cache(model: MapLocation.self).setObject(location, forKey: Keys.lastLocation.rawValue)
}
catch { }
}
}
private let disposeBag = DisposeBag()
static let shared = UserLocationManager()
var locationStream = BehaviorRelay<CLLocationCoordinate2D?>(value: nil)
// MARK: - Methods
func updateLocation() {
if CLLocationManager.locationServicesEnabled() {
locationManager.requestLocation()
}
}
func subscribe() {
locationStream.accept(lastPosition?.clCoordinate2D)
locationStream.subscribe(onNext: { [weak self] location in
guard let `self` = self else { return }
guard let location = location else { return }
self.lastPosition = MapLocation(latitude: location.latitude, longitude: location.longitude)
}).disposed(by: disposeBag)
locationManager.delegate = self
}
// MARK: - Lifecycle
override init() {
super.init()
defer {
self.subscribe()
}
}
}
// MARK: - CLLocationManagerDelegate
extension UserLocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.first else { return }
UserLocationManager.shared.locationStream.accept(location.coordinate)
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
}
}
There's no problem conceptually of having a global stream that can be subscribed to. However, your specific implementation is worrisome to me.
One of the cool things about Observable streams is that they are lazy, no work is done unless needed, but you are writing extra code to bypass that feature and I don't think it's necessary. Also, storing there current location when the app goes into the background and just assuming that is a valid location when the app comes back to the foreground (possibly weeks later) sounds inappropriate to me.
The RxCocoa package already has an Rx wrapper for CLLocationManager. It seems to me it would be far simpler to just use it. If you only need one location update then use .take(1). I'd be inclined to add a filter on the accuracy of the location before the take(1).

Resources