SwiftUI - Use EnvironmentObject in ObservableObject Class/Dependency Injection - swift

I am having trouble using an #EnvironmentObject in an #ObservableObject class. Based on some research, this is not possible, as EnvironmentObject is for views only.
I have resorted to doing the following, but the value is not updated dynamically.
For example, it is initialized with the value of "A", but when I change the value in a class that is using EnvironmentObject, the value found in my ObservableObject class remains "A". It updates in all other locations that are using the #EnvironmentObject, just not the ObservableObject API class.
Is there a way to have the code in the ObservableObject API class update when the EnvironmentObject updates the published variable?
The class that needs a variable in it that operates like EnvironmentObject is the API class.
class SelectedStation: ObservableObject {
#Published var selectedStation: String = "A"
}
class API: ObservableObject {
var selectedStation: SelectedStation
init(selectedStation: SelectedStation) {
self.selectedStation = selectedStation
print(selectedStation.selectedStation)
}
///some code that will utilize the selectedStation variable
}
What exactly am I doing wrong here?

You are initializing a different version of your class. Try adding public static let shared = SelectedStation() like this:
class SelectedStation: ObservableObject {
#Published var selectedStation: String = "A"
public static let shared = SelectedStation()
}
and then where you need to use it, declare it as:
var selectedStation = SelectedStation.shared
Also, you should rename the #Published var to something other than selectedStation, otherwise you could run into the unfortunate selectedStation.selectedStation as a reference to that variable.
Lastly, remember the #Environment needs to be initialized with the SelectedStation.shared so everything is sharing the one instantiation of the class.

Related

Managing the context of NSPersistentContainer

I have a class that manages my NSPersistentContainer for my CloudKit/CoreData. In a view to access its contents or update I use an #EnvironmentObject and call the methods and it works perfectly.
The problem is now I need to use it in a class where I can't access the environment object. So I added a shared property to the my class to access it. It works fine but the published variables don't seem to update on my views when triggered by the class.
A simple example looks as followed:
My Datacontroller:
class DataController: ObservableObject {
static let shared = DataController()
//Coredatastack would be here
let container: NSPersistentContainer
#Published var remaining: Int = 0
#Published var today: DayProgress? = nil
func getRemaining() {
remaining = Int((today?.dailyGoal ?? 0) - (today?.currDailyTotal ?? 0))
}
If I call getRemaining() anytime after updating the values of the published variable today in a view, then anywhere the environmentObject remaining is accessed will update.
But in a class if I use Datacontroller.shared.getRemaining(), it will not update the published variable remaining on the view. I assume this is because im working with different instances of the Datacontroller() but im not sure how to solve this. Thanks for any hints or tips.
Note: The class that is triggers the updates the is triggered from outside the app (shortcut) and I have a function that calls getRemainder() anytime the app opens

How Do You Pass A View Input Argument To #State Variable?

I have a basic SwiftUI question - I have a view that takes argument, in my case "symbolName" for which I would like to fetch prices. I have a class function that does this, but passing the view argument to the FetchPrice as an argument does not work. When I use a fixed string, such as "GE", it works. I am sure there is a right way to do this, thanks for any hints and tips!
Error:
Cannot use instance member 'symbolNameV' within property initializer;
property initializers run before 'self' is available
import SwiftUI
struct SymbolRow2: View {
var symbolNameV: String
#ObservedObject var fetchPrice = FetchPrice(symbolName:symbolNameV)
...
property initializers run before 'self' is available
By calling
FetchPrice(symbolName: symbolNameV)
you're accessing self. The code above is actually:
FetchPrice(symbolName: self.symbolNameV)
To solve this you can create a custom init:
struct SymbolRow2: View {
private var symbolNameV: String
#ObservedObject private var fetchPrice: FetchPrice
init(symbolNameV: String) {
self.symbolNameV = symbolNameV
self.fetchPrice = FetchPrice(symbolName: symbolNameV)
}
...
}

SwiftUI: Trouble creating a simple boolean that every other view can see

I’m loading data that appears throughout my app from a content management system's API. Once the data has finished loading, I’d like to be able to change a boolean from false to true, then make that boolean readable by every other view in my app so that I can create conditional if/else statements depending on whether the data has loaded yet or not.
I’ve tried doing this by creating an Observable Object at the root level to store the boolean, setting it to false by default:
#main
struct MyApp: App {
#StateObject var dataLoadingObject = DataHasLoaded()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(dataLoadingObject)
}
}
}
class DataHasLoaded: ObservableObject {
#Published var status: Bool = false
}
Then, in my data model file, I've tried to reference that Observable Object using #EnvironmentObject and then switch the boolean to true once the data has loaded:
class DataStore: ObservableObject {
#Published var stories: [Story] = storyData
#EnvironmentObject var dataLoadingObject: DataHasLoaded
init() {
// Fetching and appending
// the data here
self.dataLoadingObject.status = true
}
}
The app builds successfully, but when the data loads it crashes and I get this error message: Thread 1: Fatal error: No ObservableObject of type DataHasLoaded found. A View.environmentObject(_:) for DataHasLoaded may be missing as an ancestor of this view.
It seems like the issue is that my data model view isn't explicitly a child of ContentView(), so it's not aware of the Environment Object. That's where I get stuck. Do I:
a) Try to get my data model to be a child of ContentView()?
b) Use something that doesn't require a parent/child relationship?
c) Solve this some totally different way?
d) Continue to bang my head against my desk?
Thanks for your help!
#EnvironmentObject can be used in View only. It can't be used in ObservableObject.
You can pass DataHasLoaded in init instead:
class DataStore: ObservableObject {
#Published var stories: [Story] = storyData
var dataLoadingObject: DataHasLoaded
init(dataLoadingObject: DataHasLoaded) {
self.dataLoadingObject = dataLoadingObject
...
}
}
Just make sure you always pass the same instance.

SwiftUI bind array of objects and show changes

I'm trying to build a little app in SwiftUI. It is supposed to show a list of items an maybe change those. However, I am not able to figure out, how the data flow works correctly, so that changes will be reflected in my list.
Let's say I have a class of Item like this:
class Item: Identifiable {
let id = UUID()
var name: String
var dateCreated: Date
}
And this class has an initializer, that assigns each member a useful random value.
Now let's say I want to store a list of items in another class like this:
class ItemStore {
var items = [Item]()
}
This item store is part of my SceneDelegate and is handed to the ContextView.
Now what I want to do is hand one element to another view (from the stack of a NavigationView), where it will be changed, but I don't know how to save the changes made so that they will be reflected in the list, that is shown in the ContextView.
My idea is to make the item store an environment object. But what do I have to do within the item class and how do I have to pass the item to the other view, so that this works?
I already tried something with the videos from Apple's WWDC, but the wrappers there are deprecated, so that didn't work.
Any help is appreciated. Thanks a lot!
The possible approach is to use ObservableObject (from Combine) for storage
class ItemStore: ObservableObject {
#Published var items = [Item]()
// ... other code
}
class Item: ObservableObject, Identifiable {
let id = UUID()
#Published var name: String
#Published var dateCreated: Date
// ... other code
}
and in dependent views
struct ItemStoreView: View {
#ObservedObject var store: ItemStore
// ... other code
}
struct ItemView: View {
#ObservedObject var item: Item
// ... other code
}

Swift: ObservableObject, initializer doesn't define all properties?

Currently I have the code below which I'm trying to use as a navigation switch so I can navigate through different views without using the crappy NavigationLinks and otherwise. I'm by default a WebDev, so I've been having a mountain of issues transferring my knowledge over to Swift, the syntax feels completely dissimilar to any code I've written before. Anyways, here's the code;
import Foundation
import Combine
import SwiftUI
class ViewRouter: ObservableObject {
let objectWillChange: PassthroughSubject<ViewRouter,Never>
#Published var currentPage: String = "page1" {
didSet {
objectWillChange.send(self)
}
}
init(currentPage: String) {
self.currentPage = currentPage
}
}
As you can see, it's really simple, and I just use the object to switch values and display different views on another file, the only errors which prevent me from building it is the fact that the initializer is saying "Return from initializer without initializing all stored properties", even though the only variable is the currentPage variable which is defined. I know it's saying that objectWillChange is not defined by the message, but objectWillChange doesn't have any value to be assigned. Any help would be appreciated.
You just declare objectWillChange, but don't initialise it.
Simply change the declaration from
let objectWillChange: PassthroughSubject<ViewRouter,Never>
to
let objectWillChange = PassthroughSubject<ViewRouter,Never>()
However, using a PassthroughSubject shouldn't be necessary. currentPage is already #Published, so you can simply subscribe to its publisher. What you are trying to achieve using a PassthroughSubject and didSet is already defined by the swiftUI property wrappers, ObservableObject and Published.
class ViewRouter: ObservableObject {
#Published var currentPage: String
init(currentPage: String) {
self.currentPage = currentPage
}
}
Then you can simply do
let router = ViewRouter(currentPage: "a")
router.$currentPage.sink { page in print(page) }
router.currentPage = "b" // the above subscription prints `"b"`