0

In a SwiftUI view, by default FetchRequest fetches from the managedObjectContext environment value. If instead the view wants to fetch into an isolated context, for example to make discardable edits without polluting the context of other views, how can it change the context that FetchRequest uses?

One option is to wrap the view in an outer view that creates the isolated context and then calls the wrapped view using it:

var body: some View {
    WrappedView().environment(\.managedObjectContext, isolatedContext)
}

This is tedious, however. You have to create two views and pass all the wrapped views' arguments through the wrapper. Is there a better way to tell FetchRequest which context to use?

Edward Brey
  • 40,302
  • 20
  • 199
  • 253

1 Answers1

2

If you use the standard PersistentController that Apple gives as a startup you could try using

.environment(\.managedObjectContext, privateContext)

Your View would need this property to make it work. @State shouldn't be necessary since the changes are being done in the background by other means such as notifications.

let privateContext = PersistenceController.shared.container.newBackgroundContext()

Invoking newBackgroundContext() method causes the persistent container to create and return a new NSManagedObjectContext with the concurrencyType set to NSManagedObjectContextConcurrencyType.privateQueueConcurrencyType. This new context will be associated with the NSPersistentStoreCoordinator directly and is set to consume NSManagedObjectContextDidSave broadcasts automatically.

Then to test it out using most of the sample code from Apple.

struct SampleSharedCloudKitApp: App {
    let privateContext = PersistenceController.shared.container.newBackgroundContext()
    var body: some Scene {
        WindowGroup {
            VStack{
                Text(privateContext.description) //Added this to match with ContentView
                ContentView()
                    .environment(\.managedObjectContext, privateContext)
                    //Once you pass the privateContext here everything below it will have the privateContext
                    //You don't need to connect it with @FetchRequest by any other means
            }
        }
    }
}

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Item>

    var body: some View {
        List {
            Text((items.first!.managedObjectContext!.concurrencyType == NSManagedObjectContextConcurrencyType.privateQueueConcurrencyType).description) //This shows true
            Text(items.first!.managedObjectContext!.description)// This description matches the parent view
            Text(viewContext.description)// This description matches the parent view

Also, something to note is that you have to set

container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump

In order for the main context to show the changes done after saving the privateContext. I put it in the PersistenceController init right after the loadPersistentStores closure.

lorem ipsum
  • 21,175
  • 5
  • 24
  • 48
  • Is there an advantage to `let persistenceController` over just setting the `managedObjectContext` to `PersistenceController.shared.container.newBackgroundContext()` directly? – Edward Brey Apr 18 '21 at 23:25
  • I haven’t tried it but I would assume that having the property keeps it from generating a new context every time SwiftUI reloads the body. You might end up with several different contexts. – lorem ipsum Apr 18 '21 at 23:40
  • That goal makes sense, but just setting `persistenceController` won't achieve it. You need to store the local context as state: `@State private var localContext = PersistenceController.shared.container.newBackgroundContext()`. There's still the problem, however, of applying the environment to the FetchRequest, unless you force the caller to set it up or create a wrapper. – Edward Brey Apr 19 '21 at 01:30
  • Thanks. The code highlights the problem that motivated my question. If the need for a private context is an implementation detail of ContentView, it's preferable not to have that detail spill out into the containing view (the App in your example). Additionally, the containing view may not have the right lifetime. For example, if ContentView is a screen you can navigate to, you'd want a new context on each navigation, not a singleton. A wrapper view with a `@State` variable would provide the right lifetime, but it's tedious to code. – Edward Brey Apr 19 '21 at 13:37
  • Another option is to just set the `@FetchRequest` manually there are a [ton of SO questions](https://stackoverflow.com/search?q=swiftui+fetchrequest+dynamic+predicate) for a dynamic predicate but if you add `fetchRequest.includesPendingChanges = false` your results should not include the changes until they are saved. I am not sure if it works though. I tend to use the "old school" `NSFetchedRequestController` I don't like how limited the wrapper is and SwiftUI Views end up with so much persistent code. – lorem ipsum Apr 19 '21 at 13:53