I've run into a problem with the following setup:
I have a Core Data database with parent objects with a relationship to many child objects. I have a main view using SwiftUI FetchedResults that displays the parent objects in a list and properties about the child objects. I have a second view that lets you change properties of the child objects.
The parent object has the property: name (string). The child object has: name (string) and star (bool). On the main view I display a list of parent objects and how many child objects are starred. On the second view I update if a child object is starred. Upon updating the property on the second view and saving, the first view does not update until you relaunch the app.
Here is the main view:
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(sortDescriptors: [SortDescriptor(\.name)])
private var parents: FetchedResults<Parent>
@State private var editingParent: Parent?
var body: some View {
List {
ForEach(parents) { parent in
VStack(alignment: .leading) {
Text(parent.name ?? "No Name")
Text("\(parent.children!.count) children")
Text("\(getNumberOfStar(parent: parent)) Stared")
}
.onTapGesture {
editingParent = parent
}
}
}
.sheet(item: $editingParent) { parent in
ChildrenView(parent: parent)
}
Button {
PersistenceController.shared.saveParent(name: "New Parent")
} label: {
Text("Add Parent")
}.padding()
Button {
parents.forEach { parent in
PersistenceController.shared.delete(parent: parent)
}
} label: {
Text("Delete All")
}.padding()
}
//this function does not get called when the database changes
func getNumberOfStar(parent: Parent) -> Int {
let children = parent.children!.allObjects as! [Child]
let starred = children.filter({$0.star == true})
return starred.count
}
}
This is the child view:
@FetchRequest var children: FetchedResults<Child>
var parent: Parent
init(parent: Parent) {
self.parent = parent
_children = FetchRequest<Child>(sortDescriptors: [], predicate: NSPredicate(format: "parent = %@", parent))
}
var body: some View {
List {
ForEach(children) { child in
HStack {
Text(child.name ?? "No Name")
Text(child.star ? "⭐️" : "")
Spacer()
Button {
PersistenceController.shared.updateStar(child: child)
} label: {
Text("Toggle Star")
}
}
}
}
Button {
PersistenceController.shared.saveChild(name: "New Child", parent: parent)
} label: {
Text("Add Child")
}
}
}
This is the Persistence Controller:
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "Model")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
func save() {
let viewContext = container.viewContext
do {
try viewContext.save()
} catch {
/**
Real-world apps should consider better handling the error in a way that fits their UI.
*/
let nsError = error as NSError
fatalError("Failed to save Core Data changes: \(nsError), \(nsError.userInfo)")
}
}
func saveParent(name: String) {
let newParent = Parent(context: container.viewContext)
newParent.name = name
save()
}
func saveChild(name: String, parent: Parent) {
let newChild = Child(context: container.viewContext)
newChild.name = name
newChild.parent = parent
newChild.star = false
save()
}
func updateStar(child: Child) {
child.star.toggle()
save()
}
func delete(parent: Parent) {
let viewContext = container.viewContext
viewContext.delete(parent)
save()
}
}
Finally here is the App struct:
struct Core_Data_Child_Save_ExampleApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}