I have entities that all have createdAt and updatedAt properties. What I am trying to do is to set the value of those attributes automatically upon insert/update.
After doing some research, I found out that there is an awakeFromInsert method which I could use to set the value of the createdAt property automatically when a new object is created.
However, I could not find anything similar for updating an object. So what should I do? Do I have to update the updatedAt property manually every time?
It depends what you want to achieve. Updating only on save you could do at:
-(void)willSave;
Remember that modifying properties at this place will call willSave again. So, you have to update updatedAt only once. Also, You have to check that the object wasn’t deleted - isDeleted.
You may also observe all properties and set updatedAt to date when it was really updated, not saved.
I ended up finding similar questions/answers on StackOverflow (https://stackoverflow.com/a/10723861/123016 and https://stackoverflow.com/a/4590190/123016). I adapted the suggested solutions to suit my needs.
What I first did, is create an abstract entity which I used as the parent class of all other entities. This abstract class has two properties: createdAt and updatedAt.
I then added the following code to the implementation of the abstract class:
- (void)awakeFromInsert
{
[super awakeFromInsert];
self.createdAt = [NSDate date];
}
+ (void)load
{
#autoreleasepool {
[[NSNotificationCenter defaultCenter] addObserver:(id)self.class
selector:#selector(objectContextWillSave:)
name:NSManagedObjectContextWillSaveNotification
object:nil];
}
}
+ (void)objectContextWillSave:(NSNotification *)notification
{
NSManagedObjectContext *context = [notification object];
NSSet *allObjects = [context.insertedObjects setByAddingObjectsFromSet:context.updatedObjects];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"self isKindOfClass: %#", [self class]];
NSSet *allModifiableObjects = [allObjects filteredSetUsingPredicate:predicate];
[allModifiableObjects makeObjectsPerformSelector:#selector(setUpdatedAt:) withObject:[NSDate date]];
}
Apart from the fact that I made the parent class abstract, this is exactly the same code as both answers combined.
Related
I have a NSTableView populated by a Core Data entity and Add Item / Remove Item buttons all wired with a NSArrayController and bindings in Interface Builder.
The Undo/Redo menu items can undo or redo the add / remove item actions.
But the menu entries are called only „Undo“ resp. „Redo“.
How can i name them like „Undo Add Item“, „Undo Remove Item“, etc.
(I am aware, something similar was asked before, but the accepted answers are either a single, now rotten link or the advice to subclass NSManagedObject and override a method that Apples documentation says about: "Important: You must not override this method.“)
Add a subclass of NSArrayController as a file in your project. In the xib, in the Identity Inspector of the array controller, change the Class from NSArrayController to your new subclass.
Override the - newObject method.
- (id)newObject
{
id newObj = [super newObject];
NSUndoManager *undoManager = [[[NSApp delegate] window] undoManager];
[undoManager setActionName:#"Add Item"];
return newObj;
}
Also the - remove:sender method.
- (void)remove:(id)sender
{
[super remove:sender];
NSUndoManager *undoManager = [[[NSApp delegate] window] undoManager];
[undoManager setActionName:#"Remove Item"];
}
Register for NSManagedObjectContextObjectsDidChangeNotification:
[[NSNotificationCenter defaultCenter] addObserver: self
selector: #selector(mocDidChangeNotification:)
name:NSManagedObjectContextObjectsDidChangeNotification
object: nil];
And parse the userInfo dictionary in the corresponding method:
- (void)mocDidChangeNotification:(NSNotification *)notification
{
NSManagedObjectContext* savedContext = [notification object];
// Ignore change notifications for anything but the mainQueue MOC
if (savedContext != self.managedObjectContext) {
return;
}
// Ignore updates -- lots of noise from maintaining user-irrelevant data
// Set actionName for insertion
for (NSManagedObject* insertedObject in
[notification.userInfo valueForKeyPath:NSInsertedObjectsKey])
{
NSString* objectClass = NSStringFromClass([insertedObject class]);
savedContext.undoManager.actionName = savedContext.undoManager.isUndoing ?
[NSString stringWithFormat:#"Delete %#", objectClass] :
[NSString stringWithFormat:#"Insert %#", objectClass];
}
// Set actionName for deletion
for (NSManagedObject* deletedObject in
[notification.userInfo valueForKeyPath:NSDeletedObjectsKey])
{
NSString* objectClass = NSStringFromClass([deletedObject class]);
savedContext.undoManager.actionName = savedContext.undoManager.isUndoing ?
[NSString stringWithFormat:#"Insert %#", objectClass] :
[NSString stringWithFormat:#"Delete %#", objectClass];
}
}
I've tested this in my own code-- it's rough. Can spend a lot more time making the actionName nicer. I deleted parsing of updates because: 1) insertions and deletions of objects in to-many relationships generate updates of other objects 2) I don't care to figure out how to discover what properties changed at this time
I also have class names that aren't user-friendly, so this is a great time to implement the description function for all entities, and use that rather than the class name.
But this at least works for all object controllers in a project, and easily enough for insert and delete.
[edit] Updated with mikeD's suggestion to cover redo having an inverse name. Thanks!
Part of my iOS project polls a server for sets of objects, then converts and saves them to Core Data, to then update the UI with the results. The server tasks happens in a collection of NSOperation classes I call 'services' that operate in the background. If NSManagedObject and its ~Context were thread safe, I would have had the services call delegate methods on the main thread like this one:
- (void)service:(NSOperation *)service retrievedObjects:(NSArray *)objects;
Of course you can't pass around NSManagedObjects like this, so this delegate method is doomed. As far as I can see there are two solutions to get to the objects from the main thread. But I like neither of them, so I was hoping the great StackOverflow community could help me come up with a third.
I could perform an NSFetchRequest on the main thread to pull in the newly added or modified objects. The problem is that the Core Data store contains many more of these objects, so I have to add quite some verbosity to communicate the right set of objects. One way would be to add a property to the object like batchID, which I could then pass back to the delegate so it would know what to fetch. But adding data to the store to fix my concurrency limitations feels wrong.
I could also collect the newly added objects' objectID properties, put them in a list and send that list to the delegate method. The unfortunate thing though is that I have to populate the list after I save the context, which means I have to loop over the objects twice in the background before I have the correct list (first time is when parsing the server response). Then I still only have a list of objectIDs, which I have to individually reel in with existingObjectWithID:error: from the NSManagedObjectContext on the main thread. This just seems so cumbersome.
What piece of information am I missing? What's the third solution to bring a set of NSManagedObjects from a background thread to the main thread, without losing thread confinement?
epologee,
While you obviously have a solution you are happy with, let me suggest that you lose some valuable information, whether items are updated, deleted or inserted, with your mechanism. In my code, I just migrate the userInfo dictionary to the new MOC. Here is a general purpose routine to do so:
// Migrate a userInfo dictionary as defined by NSManagedObjectContextDidSaveNotification
// to the receiver context.
- (NSDictionary *) migrateUserInfo: (NSDictionary *) userInfo {
NSMutableDictionary *ui = [NSMutableDictionary dictionaryWithCapacity: userInfo.count];
NSSet * sourceSet = nil;
NSMutableSet *migratedSet = nil;
for (NSString *key in [userInfo allKeys]) {
sourceSet = [userInfo valueForKey: key];
migratedSet = [NSMutableSet setWithCapacity: sourceSet.count];
for (NSManagedObject *mo in sourceSet) {
[migratedSet addObject: [self.moc objectWithID: mo.objectID]];
}
[ui setValue: migratedSet forKey: key];
}
return ui;
} // -migrateUserInfo:
The above routine assumes it is a method of a class which has an #property NSManagedObjectContext *moc.
I hope you find the above useful.
Andrew
There's a section of the Core Data Programming Guide that addresses Concurrency with Core Data. In a nutshell, each thread should have its own managed object context and then use notifications to synchronize the contexts.
After a little experimentation, I decided to go for a slight alteration to my proposed method number 2. While performing background changes on the context, keep a score of the objects you want to delegate back to the main thread, say in an NSMutableArray *objectsOfInterest. We eventually want to get to the objectID keys of all the objects in this array, but because the objectID value changes when you save a context, we first have to perform that [context save:&error]. Right after the save, use the arrayFromObjectsAtKey: method from the NSArray category below to generate a list of objectID instances, like so:
NSArray *objectIDs = [objectsOfInterest arrayFromObjectsAtKey:#"objectID"];
That array you can pass back safely to the main thread via the delegate (do make sure your main thread context is updated with mergeChangesFromContextDidSaveNotification by listening to the NSManagedObjectContextDidSaveNotification). When you're ready to reel in the objects of the background operation, use the existingObjectsWithIDs:error: method from the category below to turn the array of objectID's back into a list of working NSManagedObjects.
Any suggestions to improve the conciseness or performance of these methods is appreciated.
#implementation NSArray (Concurrency)
- (NSArray *)arrayFromObjectsAtKey:(NSString *)key {
NSMutableArray *objectsAtKey = [NSMutableArray array];
for (id value in self) {
[objectsAtKey addObject:[value valueForKey:key]];
}
return objectsAtKey;
}
#end
#implementation NSManagedObjectContext (Concurrency)
- (NSArray *)existingObjectsWithIDs:(NSArray *)objectIDs error:(NSError **)error {
NSMutableArray *entities = [NSMutableArray array];
#try {
for (NSManagedObjectID *objectID in objectIDs) {
// existingObjectWithID might return nil if it can't find the objectID, but if you're not prepared for this,
// don't use this method but write your own.
[entities addObject:[self existingObjectWithID:objectID error:error]];
}
}
#catch (NSException *exception) {
return nil;
}
return entities;
}
#end
I have an attribute modificationDate in my Entity A. I want to set its value whenever NSManagedObject is saved. However, if i try to do that in NSManagedObject willSave: method, i get an error:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Failed to process pending changes before save. The context is still dirty after 100 attempts. Typically this recursive dirtying is caused by a bad validation method, -willSave, or notification handler.' ***
So, i'm wondering, what's the best way to set the value of modificationDate?
In fact the apple docs (which are only half read in the accepted answer) don't recommend this method. They explicitly say you should use NSManagedObjectContextWillSaveNotification. An example might be:
#interface TrackedEntity : NSManagedObject
#property (nonatomic, retain) NSDate* lastModified;
#end
#implementation TrackedEntity
#dynamic lastModified;
+ (void) load {
#autoreleasepool {
[[NSNotificationCenter defaultCenter] addObserver: (id)[self class]
selector: #selector(objectContextWillSave:)
name: NSManagedObjectContextWillSaveNotification
object: nil];
}
}
+ (void) objectContextWillSave: (NSNotification*) notification {
NSManagedObjectContext* context = [notification object];
NSSet* allModified = [context.insertedObjects setByAddingObjectsFromSet: context.updatedObjects];
NSPredicate* predicate = [NSPredicate predicateWithFormat: #"self isKindOfClass: %#", [self class]];
NSSet* modifiable = [allModified filteredSetUsingPredicate: predicate];
[modifiable makeObjectsPerformSelector: #selector(setLastModified:) withObject: [NSDate date]];
}
#end
I use this (with a few other methods: primary key for example) as an abstract base class for most core data projects.
From the NSManagedObject docs for willSave:
If you want to update a persistent property value, you should typically test for equality of any new value with the existing value before making a change. If you change property values using standard accessor methods, Core Data will observe the resultant change notification and so invoke willSave again before saving the object’s managed object context. If you continue to modify a value in willSave, willSave will continue to be called until your program crashes.
For example, if you set a last-modified timestamp, you should check whether either you previously set it in the same save operation, or that the existing timestamp is not less than a small delta from the current time. Typically it’s better to calculate the timestamp once for all the objects being saved (for example, in response to an NSManagedObjectContextWillSaveNotification).
So maybe something along the lines of:
-(void)willSave {
NSDate *now = [NSDate date];
if (self.modificationDate == nil || [now timeIntervalSinceDate:self.modificationDate] > 1.0) {
self.modificationDate = now;
}
}
Where you can adjust the 1.0 to reflect the minimum delta between your expected save requests.
Actually a much better way than the accepted answer would be to use primitive accessors, as suggested in NSManagedObject's Documentation
`
- (void)willSave
{
if (![self isDeleted])
{
[self setPrimitiveValue:[NSDate date] forKey:#"updatedAt"];
}
[super willSave];
}
`
Also, check whether the object is marked for deletion with -isDeleted, as -willSave gets called for those too.
There are obviously several good solutions to this question already, but I wanted to throw out a new one that worked best for one particular scenario I encountered.
(In Swift:)
override func willSave() {
if self.changedValues()["modificationDate"] == nil {
self.modificationDate = NSDate()
}
super.willSave()
}
The reason I needed this is because I have the peculiar requirement of needing to sometimes set the modificationDate manually. (The reason I sometimes set the time stamp manually is because I try to keep it in sync with a time stamp on the server.)
This solution:
Prevents the infinite willSave() loop because once the time stamp is set, it will appear in changedValues()
Doesn't require using observation
Allows for setting the time stamp manually
Swift 4 solution which is a combination of zmit and Richard answer without the need of recurring to NSNotification:
override func willSave() {
let expectedNewValue = "Your new value"
if customField != expectedNewValue, changedValues()[#keyPath(Entity.customField)] == nil, !isDeleted {
customField = expectedNewValue
}
super.willSave()
}
Before working with Objective-C and Core Data, I had occasions to create classes that needed to be initialized with certain parameters that, after initialization, could not be modified (though they could be read).
With Core Data, I believe I can create a customized init on my NSManagedObject derived class as long as it includes a way to insert the object into a context like such:
-(Cell*) initWithOwner:(CellList*)ownerCellList andLocation:(int)initLocation
{
if (self = (Cell*) [NSEntityDescription insertNewObjectForEntityForName:#"Cell"
inManagedObjectContext:[ownerCellList managedObjectContext]])
{
self.location = [NSNumber numberWithInt:initLocation];
self.owner = ownerCellList;
[ownerCellList addCellListObject:self];
}
return self;
}
Normally, I'd have a location variable and the location property would be read-only (so once set at initialization, it could not be changed). Is there a way to get this sort of pattern with Core Data? Is there a better way I'm not thinking of?
Thanks!
You are correct. As long as your initializer calls the NSManagedObject's designated initializer, your approach is fine. You can also override the -[NSManagedObject awakeFromInsert] to perform some action after insertion (creation) or -[NSManagedObject awakeFromFetch] to perform an action (e.g. populating a cache) each time the object is faulted back into a managed object context.
Like the rest of Objective-C, there is no way to make a property truly readonly. Malicious code will likely be able to modify your property. However, in your custom class, you can declare a #property(readonly) for e.g. location. This will at least cause a warning if you try to modify the property and will signal your intent to client code.
For anyone who stumbles here, reads the comments, and wonders at the final answer, it should be something like this. Continuing with the example above, it would be:
-(Cell*) initWithOwner:(CellList*)ownerCellList andLocation:(int)initLocation
{
NSManagedObjectContext *context = [ownerCellList managedObjectContext];
NSManagedObjectModel *managedObjectModel =
[[context persistentStoreCoordinator] managedObjectModel];
NSEntityDescription *entity =
[[managedObjectModel entitiesByName] objectForKey:#"Cell"];
self = [self initWithEntity:entity inManagedObjectContext:context];
if (self)
{
self.location = [NSNumber numberWithInt:initLocation];
self.owner = ownerCellList;
[ownerCellList addCellListObject:self];
}
return self;
}
NSEntityDescription's insertNewObjectForEntityForName:inManagedObjectContext: documentation says this is roughly how it converts from a given entityName (#"Cell") and a context (from ownerCellList) to an NSManagedObject instance.
When we have an object who has a property that's generated based on other properties, usually we implement the +keyPathsForValuesAffecting{PropertyName} class method.
What I'm trying to do is basically the same thing for a property on my NSManagedObject, but traversing a relationship.
My model is simple; I have two Entities, App and Version (I'm creating an appcast-generating app). When the App's properties are changed, because I implemented the method above, the -appcast string is changed, and all bindings are updated appropriately.
However, when any properties on any of a specific App's Versions (to-many relationship) change, the -appcast property is not generated appropriately. I can haz fix/workaround?
This is a bit of a late answer, but I think it's a common situation, and the answer definitely isn't readily-apparent.
I generally watch for the managedObjectContext to change and then check if the any of the changed objects are ones that I want to look out for. So, in your NSManagedObject subclass:
// We need to register for the notification in both awakeFromFetch
// AND awakeFromInsert, since either one could be called, depending on
// if the object was previously-created or not
- (void)awakeFromFetch {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(managedObjectContextDidChange:) name: NSManagedObjectContextObjectsDidChangeNotification object:[self managedObjectContext]];
}
- (void)awakeFromInsert {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(managedObjectContextDidChange:) name: NSManagedObjectContextObjectsDidChangeNotification object:[self managedObjectContext]];
}
- (void)managedObjectContextDidChange:(NSNotification *)notification {
// Get a set containing ALL objects which have been changed
NSSet *insertedObjects = [[notification userInfo] objectForKey:NSInsertedObjectsKey];
NSSet *updatedObjects = [[notification userInfo] objectForKey:NSUpdatedObjectsKey];
NSSet *deletedObjects = [[notification userInfo] objectForKey:NSDeletedObjectsKey];
NSSet *changedObjects = [insertedObjects setByAddingObjectsFromSet:updatedObjects];
changedObjects = [changedObjects setByAddingObjectsFromSet:deletedObjects];
if ([changedObjects intersectsSet:[self versions]]) {
[self willChangeValueForKey:#"appCast"];
[self didChangeValueForKey:#"appCast"];
}
}
This is certainly not ideal from a performance perspective, since this notification is going to fire every time anything in your object graph changes, but I've found it to be the most straightforward way to accomplish this.