Managing the context of NSPersistentContainer - swift

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

Related

SwiftUI - Use EnvironmentObject in ObservableObject Class/Dependency Injection

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.

How do I update my other views when a coreData value changes?

As of now in my profile view, when a user updates their macroGoals I want the other views to be able to load them instantly and render them into a view. Is there any sort of way of setting an environment object equal to some coreData entity values everytime a user opens the application?
So my environment object:
import Foundation
class UserInfoModel: ObservableObject {
static let shared: UserInfoModel = UserInfoModel() // <<: Here
struct DailyCalorieGoals: Identifiable{
var id = UUID()
var calorieGoal: Double
var fatGoal: Double
var proteinGoal: Double
var carbGoal: Double
}
#Published var personDailyCalorieGoals = DailyCalorieGoals.init(calorieGoal: 2400, fatGoal: 40, proteinGoal: 40, carbGoal: 40)
}
So I have two views, as tab views. When i input some data, I have some coreData values that get saved when the users leaves the application. How do I set these environment objects equal to that data when the application opens?
Maybe you could use NotificationCenter to have an #objc method run in each of the viewControllers, and post a notification whenever you're modifying the value of your CoreData model.
To answer your question regarding how to initialize a view with the latest data, look no further than viewDidLoad(), which is run every time that the app opens again.Fetch the objects from coredata, and use their values to set the value of your enviornment objects.

ObservedObject being loaded multiple times prior to navigation to its view

having a bit of trouble with the following.
I have a List in a VStack as follows:
List{
ForEach(fetchRequest.wrappedValue, id: \.self) { city in
NavigationLink(destination: CityView(city: city, moc: self.moc)) {
cityRow(city: city)
}
}
}
This list is populated from a coreData fetchrequest. Each NavigationLinks navigates to CityView and passes a city Object with it.
CityView had a observable object 'notificationHandler' defined as follows:
struct CityView: View {
#ObservedObject var notificationHandler = NotificationHandler()
#ObservedObject var city: City
var body: some View {
}
}
NotificationHandler() sets up an instance of NotificationHandler and sets up a few notification observers from within init as follows:
import Foundation
import Combine
class NotificationHandler: ObservableObject {
let nc = NotificationHandler.default
#Published var networkActive: Bool = false
init() {
nc.addObserver(self, selector: #selector(networkStart), name: Notification.Name("networkingStart"), object: nil)
nc.addObserver(self, selector: #selector(networkStop), name: Notification.Name("networkingStop"), object: nil)
}
}
My issues is this - when the app boots onto its first view which contins the list above - I'm getting a number of instance of NotificationHandler starting - one for every row of the list. - This has led me to the belief that the NavigationLinks in the list are preemtivly loading the CityView's they hold. However I believe this is no longer the case and lazy load is the defualt behaviour. To add to that adding an onAppear() within CityView shows the they are not being completly loaded.
Any help would be greatly appretiated, I can't work out how this is happening.
Thanks
The Destination on the NavigationView is NOT lazy, so it initializes the Destination view as soon as it is created. An easy work around can be found here: SwiftUI - ObservableObject created multiple times. Wrap your Destination view within the LazyView.
Would like to share the latest updates for the solution of this problem.
https://stackoverflow.com/a/66520131
This is the way by which you can avoid creating multiple instances of ObservedObjects.
Sharing this because just lazy loading in NavigationLink doesn't solve the problem where refreshing the views at runtime as per user's actions creates and destroys ObservedObject multiple times.
Hence we need some solution where our Objects are created only once and also persists even after view is refreshed.

SwiftUI not being updated with manual publish

I have a class, a “clock face” with regular updates; it should display an array of metrics that change over time.
Because I’d like the clock to also be displayed in a widget, I’ve found that I had to put the class into a framework (perhaps there’s another way, but I’m too far down the road now). This appears to have caused a problem with SwiftUI and observable objects.
In my View I have:
#ObservedObject var clockFace: myClock
In the clock face I have:
class myClock: ObservableObject, Identifiable {
var id: Int
#Publish public var metric:[metricObject] = []
....
// at some point the array is mutated and the display updates
}
I don’t know if Identifiable is needed but it’s doesn’t make any difference to the outcome. The public is demanded by the compiler, but it’s always been like that anyway.
With these lines I get a runtime error as the app starts:
objc[31175] no class for metaclass
So I took off the #Published and changed to a manual update:
public var metric:[metricObject] = [] {
didSet {
self.objectWillChange.send()`
}
}
And now I get a display and by setting a breakpoint I can see the send() is being called at regular intervals. But the display won’t update unless I add/remove from the array. I’m guessing the computed variables (which make up the bulk of the metricObject change isn’t being seen by SwiftUI. I’ve subsequently tried adding a “dummy” Int to the myClock class and setting that to a random value to trying to trigger a manual refresh via a send() on it’s didSet with no luck.
So how can I force a periodic redraw of the display?
What is MetricObject and can you make it a struct so you get Equatable for free?
When I do this with an Int it works:
class PeriodicUpdater: ObservableObject {
#Published var time = 0
var subscriptions = Set<AnyCancellable>()
init() {
Timer
.publish(every: 1, on: .main, in: .default)
.autoconnect()
.sink(receiveValue: { _ in
self.time = self.time + 1
})
.store(in: &subscriptions)
}
}
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
Text("\(self.updater.time)")
}
}
So it's taken a while but I've finally got it working. The problem seemed to be two-fold.
I had a class defined in my framework which controls the SwiftUI file. This class is sub-classed in both the main app and the widget.
Firstly I couldn't use #Published in the main class within the framework. That seemed to cause the error:
objc[31175] no class for metaclass
So I used #JoshHomman's idea of an iVar that's periodically updated but that didn't quite work for me. With my SwiftUI file, I had:
struct FRMWRKShape: Shape {
func drawShape(in rect: CGRect) -> Path {
// draw and return a shape
}
}
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
FRMWRKShape()
//....
FRMWRKShape() //slightly different parameters are passed in
}
}
The ContentView was executed every second as I wanted, however the FRMWRKShape code was called but not executed(?!) - except on first starting up - so the view doesn't update. When I changed to something far less D.R.Y. such as:
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
Path { path in
// same code as was in FRMWRKShape()
}
//....
Path { path in
// same code as was in FRMWRKShape()
// but slightly different parameters
}
}
}
Magically, the View was updated as I wanted it to be. I don't know if this is expected behaviour, perhaps someone can say whether I should file a Radar....

A #State static property is being reinitiated without notice

I have a view which looks like this:
struct Login: View {
#State static var errorMessage = ""
init() {
// ...
}
var body: some View {
// ...
}
}
I set errorMessage as static so I can set an error message from anywhere.
The problem is that even being static, it is always reinitiated each time the login view is showed, so the error message is always empty. I was thinking that maybe the presence of the init() method initiate it somehow, but I didn't figure how to fix this. What can I do?
I set errorMessage as static so I can set an error message from anywhere.
This is a misunderstanding of #State. The point of #State variables is to manage internal state to a View. If something external is even looking at a #State variable, let alone trying to set it, something is wrong.
Instead, what you need is an #ObservableObject that is passed to the view (or is accessed as a shared instance). For example:
class ErrorManager: ObservableObject {
#Published var errorMessage: String = "xyz"
}
This is the global thing that manages the error message. Anyone can call errorManager.errorMessage = "something" to set it. You can of course make this a shared instance if you wanted by adding a property:
static let shared = ErrorManager()
With that, you then pass it to the View:
struct Login: View {
#ObservedObject var errorManager: ErrorManager
var body: some View {
Text(errorManager.errorMessage)
}
}
Alternately, you could use the shared instance if you wanted:
#ObservedObject var errorManager = ErrorManager.shared
And that's it. Now change to the error automatically propagate. It's more likely that you want a LoginManager or something like that to handle the whole login process, and then observe that instead, but the process is the same.
#State creates a stateful container that is associated with an instance of a view. Each instance of your view has it's own copy of that #State container.
In contrast, a static variable does not change across instances.
These two concepts are not compatible. You should not be using static with #State.