Unreliable update with #FetchRequest predicate filtering on UUID typed attributes? - core-data

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 number attribute 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 == 5 or their UUID match
Notes:
I've used viewContext.perform liberally, 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 items collection 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)")
}
})
}
}

I believe the weird behaviour is due to your predicate, which is being evaluated in two different ways. When you first run the app, or after closing and restarting, the predicate is parsed and passed to SQLite (as a WHERE clause in a SELECT statement). Thereafter, when you add new items, the predicate is evaluated directly in memory (ie in the NSManagedObjectContext) - no need to involve SQLite.
In your predicate, you are comparing a UUID type attribute with a String value - which fails. Even if the string representation of the UUID attribute is the same as the string you compare it with, the context sees them as different and regards the new object as failing the predicate. Hence the view is not updated.
However, SQLite is much more tolerant of type mismatches. (I'm guessing CoreData's implementation of UUID attributes is to store the string representation - but that's just a guess). So when you quit and restart, the fetch is processed by SQLite which regards the new object as meeting the predicate and accordingly includes it in the results.
To get the correct behaviour, I think you need your predicate to compare the UUID attribute with the UUID which has the correct string representation:
NSPredicate(format: "number == %# OR uuid == %#", NSNumber(5), UUID(uuidString: ItemList.filterUUID))

Related

Adding to a one-to-many relationship not updating UI

I have a one-to-many relationship in core data of plan -> recipe. plan.recipes is of type NSSet?, so I have created custom NSManagedObject classes with computed properties to convert these into arrays, adding an extra property recipesArray:
public var recipesArray: [Recipe] {
let set = recipes as? Set<Recipe> ?? []
return set.sorted {
$0.wrappedName < $1.wrappedName
}
}
I then display this list in a View using a ForEach, using the recipesArray property. A subview of this view calls plan.addToRecipes(recipe: Recipe), to add a new object to the relationship. I then save.
The issue is, the ForEach in the parent view does not react to this addition. If I refresh the view by navigating away, then the new recipe is shown, but the View is not automatically updated when the new recipe is added.
Does anyone know how to do this? Should I be using the original recipes property instead of this custom array one?
You need to make another FetchRequest for Recipes using a predicate that equals a given plan, e.g. something like this:
struct RecipesView: View {
var fetchRequest: FetchRequest<Recipe>
var recipes: FetchedResults<Recipe> { fetchRequest.wrappedValue }
init(plan: Plan) {
let sortDescriptors = ...
let predicate = NSPredicate(format: "plan = %#", plan)
fetchRequest = FetchRequest(sortDescriptors: sortDescriptors, predicate: predicate, animation: .default)
}
var body: some View {
ForEach(recipes) { recipe in
RecipeView(recipe: recipe) // it's important to not access recipe's properties (firing a fault) until inside a sub-view's body.
}
}
}
Note: There is currently a bug in FetchRequest that results in body always invoked even when this View is init with the same plan and thus same FetchRequest. This is because the FetchRequest struct inits a new object inside it every time, causing it and consequently this View to appear as changed to SwiftUI. I reported this bug so hopefully they fix it. You could workaround it in the meantime with a wrapper View that takes the plan as a let so body won't be called.

How to use the new nsPredicate dynamic property of #FetchRequest property wrapper with object passed into View

My first attempt was to set the property wrapper's nsPredicate dynamic property in .onAppear, but if the view gets reinitialized for any reason, the predicate set by .onAppear is lost. So I went back to using the init pattern.
Here is what I thought should work (but doesn't) and something that does work (however mysteriously):
struct ItemEditView : View {
var item: Item
#FetchRequest(fetchRequest: Attribute.fetchRequestAllInOrder(), animation: .default)
var attributes: FetchedResults<Attribute>
init(item: Item) {
self.item = item
// This is how I would have expected to set the dynamic property at View initialization, however
// it crashes on this statement
attributes.nsPredicate = NSPredicate(format: "item == %#", item)
// Not sure why the below works and the above does not.
// It seems to work as desired, however it receives this runtime warning:
// "Context in environment is not connected to a persistent store coordinator"
$attributes.projectedValue.wrappedValue.nsPredicate = NSPredicate(format: "item == %#", item)
}
var body: some View {
List {
ForEach(attributes) { attribute in
Text("Name:\(attribute.name) Order:\(attribute.order)")
}
}
}
}
So, why does the first assignment to nsPredicate crash? And after commenting out that first one, why does the second one work? Is the warning message a real issue? Is there a better way to do this? It seems like there should be a simple way to do this using the new dynamic properties.
It turns out that (re)setting the nsPredicate property of the #FetchRequest in onAppear is really the way to go. However, to make this work, you must make sure that your View's init() method does not get called again after onAppear is called. There are several valuable hints on how to accomplish this in the Demystify SwiftUI session from this year's WWDC (WWDC21-10022).

Why does using FetchedResults work for Text and not TextField? SwiftUI, CoreData/ FetchRequest

I am getting CoreData properties from a FetchRequest and want to use it to pre-populate a text field (a user's Email).
Here is the FetchRequest
#FetchRequest(
entity: Account.entity(),
sortDescriptors:[
NSSortDescriptor(keyPath: \Account.id, ascending: true)
]
)var accounts: FetchedResults<Account>
Note I have all of the proper persistant container and #environment stuff set up to use #FetchRequest like this.
Then in my View stack I have:
var body: some View{
ZStack {
Form {
Section(header: Text("EMAIL")) {
ForEach(accounts, id: \.self) {account in
TextField("Email", text:account.email) // This is where I get an error (see error below)
}
}
}
Error is : Cannot convert value of type 'String' to expected argument type 'Binding<String>'
However is I simply list accounts in a textfield it works. Like so:
var body: some View{
ZStack {
Form {
List(accounts, id: \.self) { account in
Text(account.email ?? "Unknown")
}
}
}
Why does the second code that uses List not give me the same error?
I thought it had something to do with the ?? operator but after research I realized that it perfectly fine to do given that email in my coredata object is String?.
Now my thought is that I am getting this error here because TextField needs a Binding wrapper? If that is true I'm not sure how to get this to work. All I want to do is have this TextField pre-populated with the single email record the FetchRequest retrieves from my Account Core Data object.
Thanks.
Edit: I want to add that I have found this post https://www.hackingwithswift.com/quick-start/swiftui/how-to-fix-cannot-convert-value-of-type-string-to-expected-argument-type-binding-string
and now I think what I need to do is store this account.email result into a State variable. My question still remains however, I'm not sure how to do this as I am looking for clean way to do it right in the view stack here. Thanks again.
TextField needs a binding to a string variable. Account is and ObservableObject, like all NSManagedObjects, so if you refactor your TextField into a separate View you could do this:
Form {
Section(header: Text("EMAIL")) {
ForEach(accounts, id: \.self) { account in
Row(account: account)
}
}
}
...
struct Row: View {
#ObservedObject var account: Account
var body: some View {
TextField("Email", text: $account.email)
}
}
Note the $account.email — the $ turns this into a binding.
Unfortunately, this new code also fails, because email is a String? (i.e., it can be nil) but TextField doesn’t allow nil. Thankfully that’s not too hard to fix, because we can define a custom Binding like so:
struct Row: View {
#ObservedObject var account: Account
var email: Binding<String> {
Binding<String>(get: {
if let email = account.email {
return email
} else {
return "Unknown"
}
},
set: { account.email = $0 })
}
var body: some View {
TextField("Email", text: email)
}
}
Notice we don’t have a $ this time, because email is itself a Binding.
This answer details getting a CoreData request into a #State variable:
Update State-variable Whenever CoreData is Updated in SwiftUI
What you'll end up with is something like this:
#State var text = ""
In your view (maybe attached to your ZStack) an onReceive property that tells you when you have new CoreData to look at:
ZStack {
...
}.onReceive(accounts.publisher, perform: { _ in
//you can reference self.accounts at this point
//grab the index you want and set self.text equal to it
})
However, you'll have to do some clever stuff to make sure setting it the first time and then probably not modifying it again once the user starts typing.
This could also get complicated by the fact that you're in a list and have multiple accounts -- at this point, I may split every list item out into its own view with a separate #State variable.
Keep in mind, you can also write custom bindings like this:
Binding<String>(get: {
//return a value from your CoreData model
}, set: { newValue in })
But unless you get more clever with how you're returning in the get section, the user won't be able to edit the test. You could shadow it with another #State variable behind the scenes, too.
Finally, here's a thread on the Apple Developer forums that gets even more in-depth about possible ways to address this: https://developer.apple.com/forums/thread/128195

Index Beyond Bounds / EXC_BAD_ACCESS upon Core Data Delete SwiftUI

I'm getting a Index Beyond Bounds error and a Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT) when trying to delete a Core Data index in SwiftUI.
Basically, I have a Core Data entity (Dates), containing only a date attribute (Constraint - String). This has a one-to-many relationship with my Records Entity. I am trying to display a list of all date's I have. Displaying is fine, but upon trying to delete it, my app crashes.
My View currently looks as following:
import SwiftUI
struct Settings: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: Dates.entity(), sortDescriptors: []) var dates: FetchedResults<Dates>
var body: some View {
VStack {
List{
ForEach(dates, id: \.self) { day in
Text("\(day.wrappedDate)")
}.onDelete { (indexSet) in
let dateToDelete = self.dates[indexSet.first!]
self.managedObjectContext.delete(dateToDelete)
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
}
}
}
}
}
I've broken my view down to the bare minimum to see if that would help, but unfortunately not.
When trying to delete. The error I get in the output is:
2020-04-29 16:08:23.980755+0300 TESTTEST[28270:2245700] [General] *** -[__NSArray0 objectAtIndex:]: index 0 beyond bounds for empty NSArray
If I have say 9 dates, it would say index 8 beyond bounds [0 .. 7], so it's not necessarily related to an empty Array.
Further output is:
=== AttributeGraph: cycle detected through attribute X ===
a bunch of times, followed by:
Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
on my AppDelegate.
Could the issue be in generating the view - rather than the delete?
Please be aware that I'm a self-taught, absolute Noob when it comes to coding, so I might be missing something obvious here. Any help on getting to the answer myself in the form of instructions would also be greatly appreciated (so I can learn how to fix this).
EDIT:
I think I found out what's causing the issue. In another view I'm also generating a list of all dates , where I apply an index on Dates. Will amend code now to see if this fixes it....
TBC!
In a separate view I was calling a list of dates in the following way:
ForEach(0 ..< self.dates.count, id: \.self) { index in
Text("\(self.dates[index].date)")
}
Deleting one of the Date entities, would mess up the indices presented in this view. Changing this structure to the below fixed the issue:
ForEach(self.dates, id: \.self) { day in
Text("\(day.date)")
}

How do I transform SwiftUI fetch request results based on related objects?

I am building a SwiftUI list where I need a dynamic predicate. The approach is discussed here: https://www.hackingwithswift.com/books/ios-swiftui/dynamically-filtering-fetchrequest-with-swiftui
Here is my code so far:
struct SomeView: View {
var collection: Collection
var messages: FetchRequest<Message>
init(collection: Collection) {
let predicate : NSPredicate = NSPredicate(format: "collection = %#", collection)
self.collection = collection
self.messages = FetchRequest<Message>(entity: Message.entity(), sortDescriptors: [NSSortDescriptor(key: "date", ascending: true)], predicate: predicate)
}
var body: some View {
List(messages.wrappedValue, id: \.uniqueIdentifier) { message in
// construct the UI
}
}
}
So far so good.
What I can’t figure out how to do: I need to transform the messages elements based on some other messages in the results (let’s say based on previous message for simplicity). messages[0] should look a particular way. messages[1] look depends on messages[0]. messages[2] depends on messages[1] and so on. I cannot precompute this, since it may vary across time. It should be computed in the context of this specific fetch request/result.
I could express this as some transient computed property on the Message object, which the view code could then use to branch out. I could have a function where I give a particular message and the array of messages, the function looks up the message and other messages and sets the state of a given message based on that. However, SwiftUI limits what I can do in View code, I can’t execute functions this way.
I can run map or flatmap where I access the wrappedValue, but those don’t let me access other elements of the collection to make decisions (I think?).
How would I run this kind of transformation in this context?
If I correctly understood your description (and taking into account that FetchedResults is a RandomAccessCollection) I would go with the following approach
var body: some View {
List(messages.wrappedValue, id: \.uniqueIdentifier) { message in
rowView(for: message, from: messages.wrappedValue)
}
}
func rowView(for message: Message, from result: FetchedResults<Message>) -> some View {
// having .starIndex, .endIndex, .position, etc. do any dependent calculations here
// and return corresponding View
}

Resources