1

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?

Community
  • 1
  • 1
dschu
  • 4,992
  • 5
  • 31
  • 48

2 Answers2

7

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.

Procrastin8
  • 4,193
  • 12
  • 25
1

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.

LuLuGaGa
  • 13,089
  • 6
  • 49
  • 57
  • This is exactly what I'm already doing. I've updated my question to be more clear about that. Once I remove the `.environment()` parameter inside `ContentView`, the App will crash, because there is no context loaded. But `ProjectsView` should inherit its environment from its super view `ContentView` – dschu Sep 26 '19 at 22:11
  • Your edited code still contains ProjectsView().environment(\.managedObjectContext, managedObjectContext) as destination. It should be ProjectView() only – LuLuGaGa Sep 28 '19 at 10:02
  • Yes, because ist crashes without setting the.environment() which is the root of my question. – dschu Sep 28 '19 at 11:38
  • Does it crash in canvas, simulator or on device? – LuLuGaGa Sep 28 '19 at 11:52
  • The canvas is imho still very buggy. I'm talking about the simulator / device. It crashes as soon as I navigate to the ProjectsView. The error message is: `Context in environment is not connected to a persistent store coordinator: `. (But manually passing it, using the .environment() modificator works) – dschu Sep 28 '19 at 11:58
  • I think it might be the way your CoreData stack is setup and/or the fact that you erase your ContentView to AnyView (which looks unnecessary anyway) – LuLuGaGa Sep 29 '19 at 16:14
  • 1
    It's actually necessary because bizarrely `WKHostingController` does not mirror `UIHostingController`. You can't instantiate the hosting controller with a pre-formed view (which would implicitly set the type of `Body`) but rather you have to _override_ it and declare the generic type yourself. – Procrastin8 Oct 04 '19 at 18:24