How to tell NSPredicate skip entities before context.save() - swift

How to prevent firing controller(:didChangeContentWith: diff) before viewContext was saved?
After creating an NSManagedObject object i'm getting controller(:didChangeContentWith: diff) fired, and after saving context too, so it fires twice for the same object.
First of all, I create NSFetchedResultsController and set fetchRequest.predicate
fetchedResultsController.fetchRequest.predicate = nil
Then when I add item:
let newItem = Item(context: context) //controller fires right after this #1
...
...
context.save() // Fires here too #2
First time objectID is wrong yet, second time it's correct and set up according to the context. So, is there any way to tell predicate to skip entities, that was created before context.save()?

The only way I've found for now is filtering results. It works for me because I have own custom logic of UI updating, not sure it will work for everyone.
lists = fetchLists().filter { !$0.objectID.isTemporaryID }
Where lists is [NSManagedObject] (subclass). So when CoreData was changed, I simply fetch with needed predicate and filter out all temporary IDs. When item gets correct ID, controller(:didChangeContentWith: diff) fires again and everything works fine.
Still looking for the way to fetch with skipping isTemporaryID objects.

Related

Child Context - Deleting Object Without Save?

I'm creating a recipe object inside of a child context. I'm adding ingredients, which are a relational object to recipe.
lazy var childRecipe: Recipe = { return Recipe(context: self.childContext) }()
let newIngredient = Ingredient(context: self.childContext)
newIngredient.parent = childRecipe
newIngredient.percentage = 0.0
I'm trying to figure out how to remove/delete an ingredient that's already connected to childRecipe. This is what I've tried, but it's not working.
var ingredients = getIngredients()
let ingredientToDelete = ingredients[indexPath.row]
self.childContext.delete(ingredientToDelete)
I believe this isn't working because the context isn't being saved. But my problem is that I'm not ready to save. I just want to remove the ingredient that's not needed.
Thank you.
My cellForRowAt method had a section that taps into Recipe and gets it's relational Ingredient objects for display.
Turns out what I needed to do was remove the parent relation of Ingredient to Recipe. Which then allowed the section to return the correct number of rows. This then allowed the ingredientToDelete to be marked for delation without issue.
let ingredientToDelete = ingredients[indexPath.row]
newIngredient.parent = nil
self.childContext.delete(ingredientToDelete)

CoreData is it possible to safely use NSManagedObject from one context/thread in another context/background task

Is it possible to use NSManagedObjects between different background Task?
I have such code that when executed loads NSManagedObjects using load() method on one context/background task using inside .performBackgroundTask { context
Then I want to execute another .performBackgroundTask { context but the problem is that NSManagedObject seems to be there unavailable as all properties starting from .performBackgroundTask { stop returning fields
self.load(predicate: NSPredicate(format: "company.id = %#", companyId))
.flatMap { contacts in
Future { promise in
self.storage.persistentContainer.performBackgroundTask { context in
let request : NSFetchRequest<Company> = Company.fetchRequest()
request.predicate = NSPredicate(format: "id = %#", companyId)
let result = try? context.fetch(request)
I recommend you to read Core Data concurrency overview to understand the basics.
Regardless of your problem...
You can't directly pass Managed Objects between contexts/threads.
What you can do is to pass objectIDs off objects from one context and use them to fetch the objects from another context/thread.
Performance-wise it might not be a good idea if there are many objects and I recommend you to fetch both contracts and companies in one performBackgroundTask, using a single context. You may pass the context as a parameter to your load function if you don't want to expose its logic.

Do you need to save ManagedObjectContext in order for a Nullify delete rule to affect destination entity?

I'm saving chessGames and chessPlayers to a DB using core data.
-Each chessGame has a to-one relationship to a white Player and black player (with a delete rule set to nullify).
-Each player has to-many relationship to gamesAsWhite and to gamesAsBlack (with a delete rule set to deny).
When ever I delete a chessGame I also try to delete the players involved in that game, if the players are not involved in any other games. As shown in code below.
Code used to delete chess game and potentially players too:
context.perform {
//deletes associated chessgameSnapShot
context.delete(chessGameMO)
CoreDataUtilities.save(context: context)
//delete rule set to deny
//player only deleted if it is not involved in any games
whitePlayer.deleteIfNotInvolvedInAnyGames(inManagedObjectContext: context)
blackPlayer.deleteIfNotInvolvedInAnyGames(inManagedObjectContext: context)
CoreDataUtilities.save(context: context)
}
Implementation of deleteIfNotInvolvedInAnyGames:
func deleteIfNotInvolvedInAnyGames(inManagedObjectContext context:NSManagedObjectContext){
guard let gamesAsBlack = self.gamesAsBlack as? Set<ChessGameMO>,
let gamesAsWhite = self.gamesAsWhite as? Set<ChessGameMO> else {return}
let gamesInvolvedIn = gamesAsWhite.union(gamesAsBlack)
if gamesInvolvedIn.isEmpty {
context.delete(self)
}
}
The code only works if I save the context after I delete the chessGame. If I remove the first CoreDataUtilities.save(context: context) then the whitePlayer (and the blackPlayer) never get deleted in the deleteIfNotInvolvedInAnyGames because the relationship to chessGameMO doesn't seem to have nullified yet.
Is this normal behaviour? Shouldn't the relationships between the NSManagedObjects (in memory) be updated before I save the context?
You initial understanding is correct - validation only happens on save. So if you deleted the game and the player and then saved it should work assuming that all changes are valid. BUT the relationships are not updated until save. The relationship is still there, it is just pointing to a deleted object (object.isDeleted). So your code in deleteIfNotInvolvedInAnyGames has to filter out all deleted objects and then see if the set is empty.

Realm Notification is not triggered when NSPredicate changes

I have a simple ticked off list in Realm, where I am using NotificationToken to check for updates in the datasource and hereafter updates the tableView with the following items:
class Item : Object {
dynamic var name = ""
dynamic var isTickedOff = false
dynamic var timeStamp : Date?
}
The model:
var items: Results<Item> {
get {
let results = self.realm.objects(Item.self)
let alphabetic = SortDescriptor(property: "name", ascending: true)
let tickedOff = SortDescriptor(property: "isTickedOff", ascending: true)
let timestamp = SortDescriptor(property: "timeStamp", ascending: false)
return results.sorted(by: [tickedOff, timestamp, alphabetic]).filter("isTickedOff = %# || isTickedOff = %#", false, self.includeAll)
}
}
I have a switch in my tableview, where the user can change the self.includeAll property.
When inserting, deleting items or selecting them (resulting in setting them to isTickedOff or !isTickedOff) triggers the notification and updates the tableView. However, changing the self.includeAll property does not trigger the notification even though the items property is modified. I could include self.tableView.reloadData() when the user triggers the switch, but I would like the more smooth animations through the notification.
Is it me, who understands notifications wrong or is it a bug?
Thanks in advance!
Realm doesn't observe external properties, so it can't know when a property that is being used in a predicate query has changed, and to then subsequently generate a change notification off that.
When you access items the next time, that will give it a sufficient event to recalculate the contents, but by that point, it won't trigger a notification.
Obviously, like you said, the easiest solution would be to simply call tableView.reloadData() since that will force a refetch on items but there'll be no animation. Or conversely, like this SO question says, you can call tableView.reloadSections to actually enable a 're-shuffling' animation.
Finally, it might be a rather dirty hack, but if you still want it to trigger a Realm-based change notification, you could potentially just open a Realm write notification, change the includeAll property, and then close the write transaction to try and 'trick' Realm into performing an update.
One final point of note. I might not have enough information on this, so this might be incorrect, but if you're registering a Realm notification block off the items property, be aware that the way you've implemented that getter means that a new object is generated each time you call it. It might be more appropriate to mark that property as lazy so it's saved after the first call.

iPhone core data can I cache NSManagedObjects?

I'm running a data model in one of my apps, where an event has an "eventType" relationship defined. This allows me to modify the look and feel of multiple events by changing their "eventType" relationship object.
The problem that I'm running into is that before I insert an event, I check if a typeRelationship for this object is present with the code below. This takes some time if I need to insert a large number of objects.
Can I cache the results of this fetch request (for example in NSMutableDictionary) and check that dictionary (local memory) to see if there is an NSManagedObject with the given EventIDEnum? Can I keep the cache alive forever, or will the underlying objects get "out of date" after a while?
-(Event*)insertAndReturnNewObjectWithTypeID:(EventIDEnum)eventTypeID date:(NSDate*)date
{
NSFetchRequest *eventTypesArray = [NSFetchRequest fetchRequestWithEntityName:#"EventType"];
eventTypesArray.predicate = [NSPredicate predicateWithFormat:#"SELF.id == %d", eventTypeID];
NSArray *eventTypes = [[DataManager sharedInstance].managedObjectContext executeFetchRequest:eventTypesArray error:nil];
if(eventTypes.count==0)
{
DLog(#"ERROR inserting event with type: %i NOT FOUND",(int)eventTypeID);
return nil;
}
else {
if(eventTypes.count !=1)
{
DLog(#"ERROR found %i events with type %i",eventTypes.count,(int)eventTypeID);
}
EventType* eventType = [eventTypes lastObject];
if(date)
{
// DLog(#"Returning object");
return [self insertAndReturnNewObjectWithEventType:eventType date:date];
}else {
// DLog(#"Returning object");
return [self insertAndReturnNewObjectWithEventType:eventType];
}
}
}
Thank you for taking a look at my question!
The array of objects returned by a fetch request cannot be cached. They are only valid as long as the NSManagedObjectContext that was used to query them has not been released. The NsManagedObject.objectID and the data you retrieve from the query can be cached and kept for as long as you like. You are probably better off copying the pertinent data and objectIDs into another object you cache and maintain separately from CoreData objects; and releasing the core data array that was returned by the fetch request.
The pattern you're using is often referred to as "find or create": look for an object whose uniquing characteristic matches, return it if it exists, create/populate/return it if it didn't exist.
One thing you can do to speed this up is to do the uniquing outside of Core Data. If it's possible based on your data, perhaps you can iterate over your EventIDEnum values, find the unique values you need to have available, and thus reduce the number of fetches you perform. You'll only search once for each EventIDEnum. As long as you're working within one thread/context, you can cache those.
When I'm writing this kind of code, I find it helpful to pass in the NSManagedObjectContext as a parameter. That allows me to use the find-or-create or bulk insert methods anywhere, either on the main thread or within a private queue/context. That would take the place of your [[DataManager sharedInstance] managedObjecContext] call.