This question already has answers here:
SwiftUI - How to pass EnvironmentObject into View Model?
(7 answers)
Closed last year.
#EnviromentObject is passed down when the body is called, so it doesn't yet exist during the initialization phase of the View struct. Ok, that is clear. The question is, how do you solve the following problem?
struct MyCollectionScreen: View {
// Enviroment
#EnvironmentObject var viewContext: NSManagedObjectContext
// Internal dependencies
#ObservedObject private var provider: CoreDataProvider
init() {
provider = CoreDataProvider(viewContext: viewContext)
}
}
The previous doesn't compile and throws the error:
'self' used before all stored properties are initialized.
That is because I am trying to use the #EnviromentObject object before it is actually set.
For this, I am trying a couple of things but I am not super happy with any of them
1. Initialize my provider after the init() method
struct MyCollectionScreen: View {
// Enviroment
#EnvironmentObject var viewContext: NSManagedObjectContext
// Internal dependencies
#ObservedObject private var provider: CoreDataProvider
var body: some View {
}.onAppear {
loadProviders()
}
mutating func loadProviders() {
provider = CoreDataProvider(viewContext: viewContext)
}
}
but that, doesn't compile either and you get the following error within the onAppear block:
Cannot use mutating member on immutable value: 'self' is immutable
2. Pass the viewContext in the init method and forget about Environment objects:
This second solution works, but I don't like having to pass the viewContext to most of my views in the init methods, I kind of liked the Environment object idea.
struct MyCollectionScreen: View {
// Internal dependencies
#ObservedObject private var provider: CoreDataProvider
init(viewContext: NSManagedObjectContext) {
provider = CoreDataProvider(viewContext: viewContext)
}
}
You can make viewContext in CoreDataProvider an optional, and then set it onAppear
struct MyCollectionScreen: View {
#EnvironmentObject var viewContext: NSManagedObjectContext
#ObservedObject private var provider = CoreDataProvider()
var body: some View {
Text("Hello")
.onAppear {
provider.viewContext = viewContext
}
}
Downside is that you'll now have an optional.
I think that what I would do is to create CoreDataProvider at the same time viewContext is created, and pass it as an environment object as well.
Related
I am trying to make use of init to call the fetchProducts function in my ViewModel class. When I add init though, I am getting the following 2 errors:
Variable 'self.countries' used before being initialized
and
Return from initializer without initializing all stored properties
The variable countries is binding though so there shouldn't need to be an initialized value in this view. Am I using init incorrectly?
struct ContentView: View {
#Namespace var namespace;
#Binding var countries: [Country];
#Binding var favLists: [Int];
#State var searchText: String = "";
#AppStorage("numTimeUsed") var numTimeUsed = 0;
#Environment(\.requestReview) var requestReview
#StateObject var viewModel = ViewModel();
init() {
viewModel.fetchProducts()
}
var body: some View {
}
}
Look at the initialiser that autocomplete gives you when you use ContentView…
ContentView(countries: Binding<[Country]>, favLists: Binding<[Int]>)
If you're creating your own initialiser, it will need to take those same parameters, e.g.
init(countries: Binding<[Country]>, favLists: Binding<[Int]>) {
_countries = countries
_favLists = favLists
viewModel.fetchProducts()
}
Alternatively, use the default initialiser, and instead…
onAppear {
viewModel.fetchProducts()
}
We can use an #EnvironmentObject in SwiftUI to hold an instance of an object available for multiple views:
class MyObject: #ObservableObject {
var state = ""
func doSomethingWithState() {
//
}
}
struct MyView {
#EnvironmentObject var myObject: MyObject
}
However, we need to take care of this by adding .environment both to the main class and to every individual preview so that they don't crash:
struct MyApp: App {
var myObject = MyObject()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(myObject)
}
}
struct MyView: View {
var body: some View {
MyView()
.environmentObject(MyObject())
}
}
In addition to that there is no easy way to access one environment object from another:
class MySecondClass: #ObservableObject {
#EnvironmentObject MyObject myObject; // cannot do this
}
I came across to a better solution using Singletons with static let shared:
class MyObject: #ObservableObject {
static let shared = MyObject()
}
This was I can:
Use this object just like an #EnvironmentObject in any of my views:
struct MyView: View {
#ObservedObject var myObject = MyObject.shared
}
I don't need to add any .environment(MyObject()) to any of my views because the declaration in 1. takes care of it all.
I can easily use any of my singleton objects from another singleton objects:
class MySecondObject: #ObservableObject {
func doSomethingWithMyObject() {
let myVar = MyObject.shared.state
}
}
It seems to me better in every aspect. My question is: is there any advantage in using #EnvironmentObject over a Singleton as shown above?
The correct way is to set the environmentObject using a singleton.
struct MyApp: App {
var myObject = MyObject.shared
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(myObject)
}
}
The reason is we must not init objects inside SwiftUI structs because these structs are recreated all the time so many objects being created is a memory leak.
The other advantage to this approach is you can use a different singleton for previews:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(MyObject.preview)
}
}
FYI #StateObject are not init during previewing.
Context
To performance operation on the Core Data object, the managed object context managedObjectContext is needed. The context is passed into View via the environment variable inside SceneDelegate when the project is generated with the "using Core Data" option checked (see below). A related question is Why does Core Data context object have to be passed via environment variable?
let contentView = MainView().environment(\.managedObjectContext, context)
However, when I try to pass the context into the View Model, it complains the following
Cannot use instance member 'context' within property initializer; property initializers run before 'self' is available
struct MainView: View {
#Environment(\.managedObjectContext) var context
// Computed property cannot be used because of the property wrapper
#ObservedObject var viewModel = ViewModel(context: context)
}
class ViewModel: ObservableObject {
var context: NSManagedObjectContext
}
Adding an init() to initialize the view model inside the view causes a different error which fails the build.
Failed to produce diagnostic for expression; please file a bug report
init() {
self.viewModel = ViewModel(context: context)
}
Question
So how can I use / get / pass the context inside the view model? What's the better way to get a context inside the view model?
Here is your scenario
let contentView = MainView(context: context) // << inject
.environment(\.managedObjectContext, context)
struct MainView: View {
#Environment(\.managedObjectContext) var context
#ObservedObject private var viewModel: ViewModel // << declare
init(context: NSManagedObjectContext) {
self.viewModel = ViewModel(context: context) // initialize
}
}
The CoreData context can get via the AppDelegate object.
import SwiftUI
class ViewModel: ObservableObject {
var context: NSManagedObjectContext
init() {
let app = UIApplication.shared.delegate as! AppDelegate
self.context = app.persistentContainer.viewContext
}
}
REFERENCE, https://kavsoft.dev/Swift/CoreData/
I'm trying to assign the value from an EnvironmentObject called userSettings to a class instance called categoryData, I get an error when trying to assign the value to the class here ObserverCategory(userID: self.userSettings.id)
Error says:
Cannot use instance member 'userSettings' within property initializer; property initializers run before 'self' is available
Here's my code:
This is my class for the environment object:
//user settings
final class UserSettings: ObservableObject {
#Published var name : String = String()
#Published var id : String = "12345"
}
And next is the code where I'm trying to assign its values:
//user settings
#EnvironmentObject var userSettings: UserSettings
//instance of observer object
#ObservedObject var categoryData = ObserverCategory(userID: userSettings.id)
class ObserverCategory : ObservableObject {
let userID : String
init(userID: String) {
let db = Firestore.firestore().collection("users/\(userID)/categories") //
db.addSnapshotListener { (snap, err) in
if err != nil {
print((err?.localizedDescription)!)
return
}
for doc in snap!.documentChanges {
//code
}
}
}
}
Can somebody guide me to solve this error?
Thanks
Because the #EnvironmentObject and #ObservedObject are initializing at the same time. So you cant use one of them as an argument for another one.
You can make the ObservedObject more lazy. So you can associate it the EnvironmentObject when it's available. for example:
struct CategoryView: View {
//instance of observer object
#ObservedObject var categoryData: ObserverCategory
var body: some View { ,,, }
}
Then pass it like:
struct ContentView: View {
//user settings
#EnvironmentObject var userSettings: UserSettings
var body: some View {
CategoryView(categoryData: ObserverCategory(userID: userSettings.id))
}
}
I want to get started with Core Data & SwiftUI and therefore created a new watchOS project using the latest Xcode 11.1 GM.
Then, I copied both persistentContainer & saveContext from a fresh iOS project (with Core Data enabled), to gain Core Data capabilities.
After that I modified the HostingController to return AnyView and set the variable in the environment.
class HostingController: WKHostingController<AnyView> {
override var body: AnyView {
let managedObjectContext = (WKExtension.shared().delegate as! ExtensionDelegate).persistentContainer.viewContext
return AnyView(ContentView().environment(\.managedObjectContext, managedObjectContext))
}
}
Now I can access the context inside the ContentView, but not in its sub views.
But thats not how it is intended to be? As far as I know, all sub views should inherit its environment from its super views, right?
Right now, to access it inside its sub views I simply set the environment variables again, like this:
ContentView.swift
NavigationLink(destination: ProjectsView().environment(\.managedObjectContext, managedObjectContext)) {
HStack {
Image(systemName: "folder.fill")
Text("Projects")
}
}
Once I remove the .environment() parameter inside ContentView, the App will crash, because there is no context loaded?!
The error message is Context in environment is not connected to a persistent store coordinator: <NSManagedObjectContext: 0x804795e0>.
ProjectsView.swift
struct ProjectsView: View {
#Environment(\.managedObjectContext) var managedObjectContext
[...]
}
But again, that can't be right? So, whats causing the error here?
I was able to solve this by fixing up HostingController and guaranteeing the CoreData stack was setup before view construction. First, let's make sure the CoreData stack is ready to go. In ExtensionDelegate:
class ExtensionDelegate: NSObject, WKExtensionDelegate {
let persistentContainer = NSPersistentContainer(name: "Haha")
func applicationDidFinishLaunching() {
persistentContainer.loadPersistentStores(completionHandler: { (storeDescription, error) in
// handle this
})
}
}
I had trouble when this property was lazy so I set it up explicitly. If you run into timing issues, make loadPersistentStores a synchronous call with a semaphore to debug and then figure out how to delay nib instantiation until the closure is called later.
Next, let's fix HostingController, by making a reference to a constant view context. WKHostingController is an object, not a struct. So now we have:
class HostingController: WKHostingController<AnyView> {
private(set) var context: NSManagedObjectContext!
override func awake(withContext context: Any?) {
self.context = (WKExtension.shared().delegate as! ExtensionDelegate).persistentContainer.viewContext
}
override var body: AnyView {
return AnyView(ContentView().environment(\.managedObjectContext, context))
}
}
Now, any subviews should have access to the MOC. The following now works for me:
struct ContentView: View {
#Environment(\.managedObjectContext) var moc: NSManagedObjectContext
var body: some View {
VStack {
Text("\(moc)")
SubView()
}
}
}
struct SubView: View {
#Environment(\.managedObjectContext) var moc: NSManagedObjectContext
var body: some View {
Text("\(moc)")
.foregroundColor(.red)
}
}
You should see the address of the MOC in white above and in red below, without calling .environment on the SubView.
In each view where you want to access your managedObjectContext you need to declare it like this:
#Environment(\.managedObjectContext) var context: NSManagedObjectContext
You don't set it on views, it gets passed around for you. And don't forget to import CoreData as well in those files.