Is it possible to reinject the environment value (managedObjectContext) during runtime - core-data

I inject an NSManagedObjectContext into my view hierarchy the following way.
#main
struct MyApp: App {
var persistence: Persistence
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistence.container.viewContext)
}
}
}
In the user settings I can switch on/off the CloudKit sync mechanism. The viewContext in the Persistence instance will be reinitialized accordingly. This is working fine. But my complete view hierarchy is still using the old NSManagedObjectContext instance.
Is there any way to update (reinject) the environment value or to reload the whole view hierarchy with the newly instantiated viewContext?

Here is a possible approach - you need to make Persistence as ObservableObject and observe it in app, like
struct MyApp: App {
#StateObject var persistence: Persistence = Persistence() // or whatever init neede
...
and
class Persistence: ObservableObject {
#Published var container: NSPersistentContainer?
...
so whenever you change container the view context will be re-injected into ContentView

Related

List not updating after inserting new value from another target SwiftUI

I'm trying to setup a action extension for my app. The action extension lets you save text to the main app thought a shared CoreData container.
The problem is that when I save text from the extension and return to the main app the updated data is not automatically loaded. The data added thought the extension does show up when I restart the app.
struct RecentsView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [], predicate: nil, animation: .default)
private var masterClips: FetchedResults<Clip>
var body: some View {
NavigationView {
List {
ForEach(masterClips, id: \.self) { item in
Text(item.text!)
}
}
}
}
}

#Binding - passing FetchedResults, single Entities, to SubViews in Core Data + SwiftUI

I just want to pass a binding to an Core Data - Entity to a SwiftUI SubView screen inside a ForEach Loop with a binding, so I have access to edit properties of the Entity, I can save my context and get automatic updated views..
how can I achieve something like this:
ContentView {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(entity:ToDo.entity(), sortDescriptors: [])
private var toDoItems: FetchedResults<ToDo>
... the following part is what I need:
ForEach(toDoItems) { (item:ToDo) in
NavigationLink(
destination: MyEditView($item),
...
}
}
You need to set an #ObservedObject var item: ToDo. Your CoreData entity is a class that conforms to ObservableObject and will force a view update when any property is changed in it.
struct NextView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#ObservedObject var item: ToDo
var body: some View {
Button(action: {
item.name = "New value"
if managedObjectContext.hasChanges {
do {
try self.managedObjectContext.save()
print("SAVED CONTEXT")
} catch let error {
print("Error: SAVING CONTEXT \(error), \(error.localizedDescription)")
}
}, label: {
Text("\(item.name)")
})
}
}
You can now make any change you want and save the context when needed

Isolated managed object context for FetchRequest in SwiftUI

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

Reinitialize Combine Publishers When User Changes Global Object

I have a Core Data publisher that is working great so far. I have a Workspace entity and a Project entity. I use the following publisher to get all the projects for a given workspace:
class ProjectModel: ObservableObject {
#Published var projects = [Project]()
private var cancellableSet: Set<AnyCancellable> = []
init(){
CoreDataPublisher(request: Project.getAllProjects(), context: PersistenceController.shared.container.viewContext)
.sink(
receiveCompletion: { print($0) },
receiveValue: { [weak self] items in
self?.projects = items
})
.store(in: &cancellableSet)
}
}
The fetch request getAllProjects() is in a Core Data entity extension here where the NSPredicate filters based on a Workspace object set in the UI.
//Core Data Entity Extension
extension Project{
#nonobjc public class func getAllProjects() -> NSFetchRequest<Project> {
let workspace = AppState.shared.workspace as Workspace //<-- The user can change this workspace
let request = NSFetchRequest<Project>(entityName: "\(Self.self)")
request.sortDescriptors = [NSSortDescriptor(keyPath: \Project.name, ascending: true)]
request.predicate = NSPredicate(format: "workspace = %#", workspace)
return request
}
}
This Workspace object is in a global state class:
class AppState: ObservableObject{
static let shared = AppState()
#Published var workspace: Workspace!
init(){
//Setup the workspace for the first time
}
}
I can successfully receive data from my publisher and I can successfully change the global Workspace in the UI. The problem is that after changing the Workspace, the publisher still points to the old Workspace originally set when the fetch request was created.
How do I prompt the ProjectModel to reinitialize in order to renew the publisher's state when the AppState's workspace is changed?
Typically the way to accomplish this is to use the flatMap operator on your publisher. flatMap lets you "create a new publisher based on this received value, and then use that publisher's output as the overall publisher chain's output".
It would look something like this:
AppState.shared.$workspace.flatMap { workspace in
let request = NSFetchRequest<Project>(entityName: "\(Project.self)")
request.sortDescriptors = [NSSortDescriptor(keyPath: \Project.name, ascending: true)]
request.predicate = NSPredicate(format: "workspace = %#", workspace)
return CoreDataPublisher(request: request, context: PersistenceController.shared.container.viewContext)
}
This gives you a new publisher that:
when the .workspace property changes on your app state, constructs a new CoreDataPublisher based off whatever the new Workspace value is
uses that CoreDataPublisher as the source of values for the overall publisher stream
In your ProjectModel, I'd switch from the Set of AnyCancellable to a specific one so that you can cancel it:
var cdPublisherCancellable : AnyCancellable?
I'd move the setup of this publisher out of init, since you'll need to call it again:
func setupPublisher() {
cdPublisherCancellable?.cancel()
cdPublisherCancellable = CoreDataPublisher(request:)...
}
Then, since workspace is a published property on your shared AppState, I'd set up another publisher link to watch it:
var workspaceCancellable : AnyCancellable?
init() {
workspaceCancellable = AppState.shared.$workspace.sink { workspace in
setupPublisher()
}
}

How to create a NSManagedObject in a modal view using SwiftUI?

How does one create a new ManagedObject (MO) in a modal view using SwiftUI?
Running into a strange bug where Xcode consumes GBs of memory and fills up the hard drive on the mac through swap files.
When one creates the Modal view in the .sheet modifier there appears to be some kind of infinite loop created that fills the memory with duplicates of the ManagedObject that is injected into that Modal view.
This sample project illustrates at least part of the issue. If you run it you see the method called in the .sheet modifier fires over and over again. A theory is that the screen underneath that displays a list of ManagedObjects is causing some kind of loop between the two screens.
https://github.com/sphericalwave/ChildContextTest
Was hoping to use a childContext in the modal screen so any unsaved changes would be discarded if the modal view was dismissed without saving the childContext. But need to clear this hurdle first and there is some challenge involved in sharing ManagedObject across contexts.
import CoreData
import SwiftUI
struct CrtFdsUI: View
{
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: CrtFd.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \CrtFd.scale, ascending: true)])
var crtFds: FetchedResults<CrtFd>
#State var showModal = false
#ObservedObject var absFd: AbsFd
func crtFdModal() -> CrtFdUI {
print("func crtFdModal() -> CrtFdUI")
let cF = CrtFd(scale: 1.0, absFd: absFd, moc: moc)
return CrtFdUI(crtFd: cF)
}
var body: some View {
NavigationView {
VStack {
List {
ForEach(self.crtFds, id: \.objectID) {
CrtFdCell(crtFd: $0)
}
}
.navigationBarTitle("CrtFdsUI")
.navigationBarItems(trailing: PlusBtn(showModal: $showModal))
}
.sheet(isPresented: $showModal) { self.crtFdModal() } //FIXME: Called in endless loop?
}
}
}
Here's the list of managedObjects.
import CoreData
import SwiftUI
struct CrtFdsUI: View
{
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: CrtFd.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \CrtFd.scale, ascending: true)])
var crtFds: FetchedResults<CrtFd>
#State var showModal = false
#ObservedObject var absFd: AbsFd
func crtFdModal() -> CrtFdUI {
print("func crtFdModal() -> CrtFdUI")
let cF = CrtFd(scale: 1.0, absFd: absFd, moc: moc)
return CrtFdUI(crtFd: cF)
}
var body: some View {
NavigationView {
VStack {
List {
ForEach(self.crtFds, id: \.objectID) {
CrtFdCell(crtFd: $0)
}
}
.navigationBarTitle("CrtFdsUI")
.navigationBarItems(trailing: PlusBtn(showModal: $showModal))
}
.sheet(isPresented: $showModal) { self.crtFdModal() }
}
}
}
Right now the code creates a new instance of CrtFd every time the view hierarchy is recalculated. That's probably not a good idea since the hierarchy might be recalculated unexpectedly for reasons you don't directly control, so that even without an infinite loop of creation you'd still probably end up with more new managed objects than you want.
I downloaded your project and I'm not sure what the two Core Data entities represent, but I did notice that when CrtFdsUI first appears, its absFd already has a value for the crtFd property, and that the crtFd property is to-one rather than to-many. That means that when you create a new instance in this code, you're replacing one CrtFd with another that's an exact duplicate.
I'm going to guess that you don't really want to replace one instance with an identical duplicate, so one way of avoiding the problem, then, is to change your crtFdModal() to use the one that already exists, and only create a new copy if there isn't one already:
func crtFdModal() -> CrtFdUI {
print("func crtFdModal() -> CrtFdUI \(showModal)")
let cF = absFd.crtFd ?? CrtFd(scale: 1.0, absFd: absFd, moc: moc)
return CrtFdUI(crtFd: cF)
}
That avoids the problem you describe. It's hard to tell if it's exactly what you need, but since your code creates apparently-unnecessary duplicates it seems likely that it is.

Resources