Isolated managed object context for FetchRequest in SwiftUI - core-data

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.

Related

SwiftUI + Core Data: list doesn't refresh changes (no relationship)

Recently I have integrated Core Data to my SwiftUI App.
Just to simplify, I have two views:
TimerListView: fetch data from Core Data and show the list
TimerDetailView: show Timer details and update data
struct TimerListView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Timer.id, ascending: true)],
animation: .default)
private var timers: FetchedResults<Timer>
var body: some View {
NavigationView{
List {
ForEach(timers) { timer in
NavigationLink(destination: TimerDetailView(timer: timer)) {
TimerCardView(timer: timer)
}
}
}
}
[...]
}
}
Where
struct TimerDetailView: View {
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var timer: Timer
Outcomes
Data is correctly updated in Core Data (if I close and reopen the App data is correctly updated)
When from TimerDetailView I turn back to TimerListView (after an update) the list doesn't show the updated data
SOLVED: all child classes view have to define Timer attribute using #ObservedObject wrapper.
In previous code I replaced, into TimerCardView(timer: timer):
from var timer: Timer
to #ObservedObject var timer: Timer
And now WORKS!

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!)
}
}
}
}
}

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

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

Save updates to Core Data object

I have a Core Data object that is created in one tab and located in another via UUID. It can be updated in the second tab by clicking on the "Save" button.
Currently, when the "Save" button is clicked, the changes to the object are shown across all sheets, but when I close the app and open it again the object has gone back to how it was before.
The "Save" button currently has a similar logic to in this post. I think what could be missing is to add try viewContext.save to the "Save" button, but I'm not sure how to do this without creating a new object (as opposed to updating the existing one.
Any ideas? Thank you! (and please let me know if I need to add more context, I think this is the right amount but wasn't totally sure).
import SwiftUI
import CoreData
struct ModifySheet: View {
#Environment(\.managedObjectContext) private var viewContext
var fetchRequest: FetchRequest<Items>
var items: FetchedResults<Items> {
fetchRequest.wrappedValue
}
#State var newCost: Double = 0
var body: some View {
VStack {
Form {
Slider(value: $newCost, in: 0...100)
}
Button(action: {
items[0].cost = newCost
})
{Text("Save")}
}
}
init(filter: UUID) {
fetchRequest = FetchRequest<Item>(entity: Item.entity(), sortDescriptors: [], predicate: NSPredicate(format: "id == %#", filter as CVarArg))
}
}

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