Search code examples
swiftwatchkitswiftui

Environment variable not passed to Subviews


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?


Solution

  • 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.