Using SwiftUI, CoreData and @FetchRequest wrapper, I simply do not get any view updates on cahnegs on related entities. I set up the following simple and working example:
The CoreData model looks as follows:
GroupEntity -hasMany[items]-> ItemEntity
ItemEntity -hasOne[group]-> GroupEntity
I added two extensions to the CoreData autogenerated Classes to get around the nasty optionals here. The button at the bottom creates a group with three items in it. They're named equally but are truely different due to their unique id. The view simply lists a group and all its related items and their according value below. Tapping an item increases it's value by one. Tapping the "change GroupName" button at the bottom of each group section changes the group's name. There is no additional code than the following:
import SwiftUI
extension ItemEntity {
var wName: String { name ?? "nameless item"}
}
extension GroupEntity {
var wName: String { name ?? "nameless group" }
var aItems: [ItemEntity] {
let set = items as? Set<ItemEntity> ?? []
return set.sorted {
$0.wName < $1.wName
}
}
}
struct ContentView: View {
@Environment(\.managedObjectContext) var moc
@FetchRequest(entity: GroupEntity.entity(), sortDescriptors: []) var groups: FetchedResults<GroupEntity>
var body: some View {
List {
ForEach(groups, id: \.self) { group in
Section(header: Text("\(group.wName) >> \(group.aItems.reduce(0, {$0 + $1.value}))")) {
ForEach(group.aItems, id: \.id) { (item: ItemEntity) in
Text("\(item.wName) >> \(item.value)")
.onTapGesture {
item.value = item.value + 1
print("item value: \(item.value)")
// -> value changed & stored, but view IS NOT UPDATED
try? moc.save()
}
}
Button(action: {
group.name = group.wName + " [changed]"
// -> value changed,stored and view IS UPDATED
try? moc.save()
}) {
Text("change \(group.wName)")
}
}
}
Button(action: addGroupItems) {
Text("Add Group with Items")
}
}
}
func addGroupItems() {
let group = GroupEntity(context: moc)
group.id = UUID()
group.name = "G1"
let item1 = ItemEntity(context: moc)
item1.id = UUID()
item1.name = "I1"
item1.value = 1
group.addToItems(item1)
let item2 = ItemEntity(context: moc)
item2.id = UUID()
item2.name = "I2"
item2.value = 2
group.addToItems(item2)
let item3 = ItemEntity(context: moc)
item3.id = UUID()
item3.name = "I3"
item3.value = 3
group.addToItems(item3)
// save
try? moc.save()
}
}
Whenever the Group's name is changed, the View automatically refreshes and makes the change visible. But when tapping an item, it's value increases (as the print statement proves) but the view is not updated. Please note the additional requirement, that the group's section header also includes a computed sum of all it's item's values. This schould also be updated when an item changes. That's why simply exporting the Item into an separated itemRowView(item: item)
and declaring item as @ObservedObject var item: ItemEntity
in there is not the solution here.
I assume, that the @FetchRequest wrapper somehow ignores all changes to related items. But I cannot belief that it's that useless for related data because that's CoreDatas main power?
Thank you all for any helpful comment and idea!
Okay so I found a solution that worked for me. Adding a publisher watching for changes in the managedObjectContext and updating whenever a change isd etected does the trick.
struct ContentView: View {
private var mocDidChanged = NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange)
/* ... */
var body: some View {
List { /* .. */ }.onReceive(self.mocDidChanged) { _ in
self.refresh()
}
}
func refresh() {
/* update related state variables here */
}
}