I'm new-ish to Swift, and trying to better understand Core Data with some (I thought!) simple projects.
I'm trying to use an @FetchRequest property wrapper to list child-items which belong to a given parent. This works for most cases, but not when the to-parent relationship is the only thing which would cause a child item to be displayed after a new child item is added.
The problem I'm facing seems to boil down to any filtering done with UUID values using predicates in FetchRequest decorators not being updated the same way other attributes are. The minimal example I've (with an edit) constructed shows just the behaviour when using a UUID for the predicate, and avoids any relationship complications.
My question is, is there a 'nice' way to make this behaviour (UUID in a predicate driving a FetchRequest which updates views properly) work, or something I'm missing, like registering one of the parts as an observed item, or something I'm doing triggering an update at the wrong time?
I have produced an example which illustrates the puzzle I'm having. Note: I'm using iOS 14.4 and Xcode 12.4 I appreciate these are not current, but my machine does not support an OS past Catalina, so I'm stuck here until I can afford a new one.
My Core Data model is as follows:
The Item entity has two attributes (uuid, of type UUID) and an Int16 attribute (number).
The app has two buttons: + to add a new item with a random number value and a fixed UUID of 254DC... and 5 which adds a item, with a number value of 5 (and the same, fixed, UUID).
The predicate[s] I'm using to drive this list filter Item entities which either:
- match the hard-coded UUID value*
- or have a
numberattribute of 5
The problem is that when adding (using the + button) a new entity which should show up in this view just because its uuid property matches, it does not appear. It does appear if you quit the app, and return to it (this proves that the predicate does indeed 'match' the UUID values).
After tapping 5 (now showing two rows with number 5, as expected):
After tapping + twice (unchanged view, should have two new rows showing):
If we exit the app and then reopen it, the added Items (300 and 304 in this case) are only now visible:
Different phrasing of the question: Why do these updates work unreliably on UUID, but perfectly with the number (Int16-typed) attribute?
Working as expected:
- Deleting Item entities works as expected, regardless of whether they were in the list because of
number == 5or their UUID match
Notes:
- I've used
viewContext.performliberally, as one other glitch with an update not displaying was resolved by using that. - I had already (based on this SO question/answer) already tried wrapping each of the elements of the rows as an ObservedObject. This was also suggested in a comment. Have updated the example code to do that, which has no effect on this behaviour. (The elements are not in the
itemscollection produced by the FetchRequest, so do not get wrapped by this code.) - There is lots of
try!all over the place. This is just to keep the example code as small as possible.
View code
import SwiftUI
import CoreData
struct ItemList: View {
@Environment(\.managedObjectContext) private var viewContext
static let filterUUID = "254DC821-F654-4D45-BC94-CEEE12A428CB"
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.number, ascending: true)],
predicate: NSPredicate(format: "number == %@ OR uuid == %@", NSNumber(5), filterUUID))
var items : FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in ItemRowView(item: item) }
.onDelete(perform: deleteItems)
}
.navigationTitle(Text(items.isEmpty ? "No items" : "Item count: \(items.count)"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button(action: addRandom) {
Label("Add Item", systemImage: "plus")
}
Button(action: addFive) {
Label("Add Five", systemImage: "5.circle.fill")
}
}
}
}
}
private func addItem(number: Int) {
viewContext.perform {
let newItem = Item(context: viewContext)
newItem.uuid = UUID(uuidString: ItemList.filterUUID)
newItem.number = Int16(number)
try! viewContext.save()
}
}
private func addRandom() { addItem(number: Int.random(in: 10...500)) }
private func addFive() { addItem(number: 5) }
private func deleteItems(offsets: IndexSet) {
viewContext.perform {
offsets.map { items[$0] }.forEach(viewContext.delete)
try! viewContext.save()
}
}
}
struct ItemRowView : View {
@ObservedObject var item : Item
var body : some View {
HStack {
Text("\(item.number)")
.font(.largeTitle)
Text(item.uuid?.uuidString.prefix(8) ?? "None")
}
}
}
What have I tried already?
I've spent the best part of two days trawling documentation, StackOverflow, and various blogs, and while there have been several things which seemed on first look to be exactly the answer I was looking for, the simpler I got my example, the more confusing it became.
This started out as a similar issue related to parent-child fetching, but I've narrowed the actual problem I'm facing down to something smaller, which can be demonstrated with the single entity with an UUID, and no relationships.
Supporting code (App and PersistenceController)
import SwiftUI
import CoreData
@main
struct minimalApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ParentListView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "minimal")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}


