Detecting changes to a specific attribute of NSManagedObject
How can I detect changes to a specific attribute of an NSManagedObject
? In my Core Data data model, I have a Product
entity that represents a product for sale. The Product
entity has several attributes: price
, sku
, weight
, numberInStock
, etc. Whenever the price
attribute of a Product
changes, I need to perform a lengthy calculation. Consequently, I would like to know when the price
attribute of any Product
changes, [edit] even if that change comes from merging a context saved on another thread. What is a good way to go about doing this? I have thousands of Product
objects in my store; obviously it's not feasible to send each one an addObserver
message.
I have been using NSManagedObjectContextObjectsDidChangeNotification
to detect changes, but it only notifies me that a managed object has changed, not which attribute of that object has changed. I could redo the calculation whenever there's any change to a Product
, but that results in useless recalculations whenever an irrelevant attribute has changed. I'm con开发者_开发百科sidering making a Price
entity (that only contains a price
attribute) and using a to-one relationship between Product
and Price
. This way, I can detect changes to Price
objects in order to kick off the calculation. This seems excessively kludgy to me. Is there a better way?
Update:
@railwayparade pointed out that I could use the changedValues
method of NSManagedObject
to determine which properties have changed for each updated object. I completely missed that method, and it would totally solve my problem if the changes weren't being made on a background thread and merged into the main thread's context. (See next paragraph.)
I completely missed a subtlety about the way that NSManagedObjectContextObjectsDidChangeNotification
works. As far as I can tell, when a managed object context saved on another thread is merged into a context on the main thread (using a mergeChangesFromContextDidSaveNotification:
), the resulting NSManagedObjectContextObjectsDidChangeNotification
only contains change information about objects that are currently in the main thread's managed object context. If a changed object isn't in the main thread's context, it won't be part of the notification. It makes sense, but wasn't what I was anticipating. Therefore, my thought of using a to-one relationship instead of an attribute in order to get more detailed change information actually requires examination of the background thread's NSManagedObjectContextDidSaveNotification
, not the main thread's NSManagedObjectContextObjectsDidChangeNotification
. Of course, it would be much smarter to simply use the changedValues
method of NSManagedObject
as @railwayparade helpfully pointed out. However, I'm still left with the problem that the change notification from the merge on the main thread won't necessarily contain all of the changes made on the background thread.
One point with regard to this thread,
The NSManagedObjectContextObjectsDidChangeNotification generated by Core Data indicates that a managed object has changed, but doesn't indicate which attribute has changed.
It actually does. The "changedValues" method can be used to query which attributes changed.
Something like,
if([updatedObjects containsKindOfClass:[Config class]]){
//if the config.timeInterval changed
NSManagedObject *obj = [updatedObjects anyObject];
NSDictionary *dict=[obj changedValues];
NSLog(@"%@",dict);
if([dict objectForKey:@"timeInterval"]!=nil){
[self renderTimers];
}
}
This type of circumstance is where you need a custom NSManagedObject subclass. You need the subclass because you are adding a behavior, reacting to a price change, to the managed object.
In this case, you would override the accessor for the price
attribute. Create a custom subclass using the popup menu in the data model editor. Then select the price
attribute and choose 'Copy Obj-C 2.0 Implementation to the Clipboard`. It will give you a lot of stuff but the key bit will look like this:
- (void)setPrice:(NSNumber *)value
{
[self willChangeValueForKey:@"price"];
[self setPrimitivePrice:value];
[self didChangeValueForKey:@"price"];
}
Just add the code to deal with the price change and you are done. Anytime a specific product's price changes, the code will run.
You could take a look at KVO (Key Value Observing). Not sure if there are wrappers built into Core Data API, but I know it's part of Objective-C.
I thought I would document my design decisions here in case they're useful to others. My final solution was based on TechZen's answer.
First, I'll start with a short, and hopefully clearer, restatement of the problem:
In my application, I want to detect changes to a specific attribute (price
) of a managed object (Product
). Furthermore, I want to know about those changes whether they're made on the main or a background thread. Finally, I want to know about those changes even if the main thread currently does not have the changed Product
object in its managed object context.
The NSManagedObjectContextObjectsDidChangeNotification
generated by Core Data indicates that a managed object has changed, but doesn't indicate which attribute has changed. My kludgy solution was to create a Price
managed object containing a single price
attribute, and to replace the price
attribute in Product
with a to-one relationship to a Price
managed object. Now, whenever a change is made to a Price
managed object, the Core Data NSManagedObjectContextObjectsDidChangeNotification
will contain that Price
object in its NSUpdatedObjectsKey
set. I simply need to get this information to the main thread. This all sounds good, but there's a hitch.
My Core Data store is being manipulated by two threads. This is done in the "usual" way—there is a managed object context for each thread and a single shared persistent store coordinator. After the background thread makes changes, it saves its context. The main thread detects the context save via the NSManagedObjectContextDidSaveNotification
and merges the context changes using mergeChangesFromContextDidSaveNotification:
. (Actually, since notifications are received in the same thread they're posted in, the NSManagedObjectContextDidSaveNotification
is received on the background thread and passed to the main thread via performSelectorOnMainThread:
for merging.) As a result of the merge, Core Data generates a NSManagedObjectContextObjectsDidChangeNotification
indicating the changed objects. However, as far as I can tell, the NSManagedObjectContextObjectsDidChangeNotification
only includes those objects which are currently represented in the receiving context. This makes sense from the perspective of updating the UI. If a managed object isn't being displayed, it probably won't be in the context, so there's no need to include it in the notification.
In my case, my main thread needs to know about changes made to managed objects whether or not they're currently in the main thread's context. If any price changes, the main thread needs to queue an operation to process that price change. Therefore, the main thread needs to know about all price changes even if those changes are made on a background thread to a product that's not currently being accessed on the main thread. Obviously, since NSManagedObjectContextObjectsDidChangeNotification
only contains information about objects currently in the main thread's context, it doesn't meet my needs.
The second option I thought of was to use the NSManagedObjectContextDidSaveNotification
generated by the background thread when it saves its context. This notification contains information about all changes to managed objects. I already detect this notification and pass it to the main thread for merging, so why not peek inside and see all of the managed objects that have changed? You'll recall that managed objects are not meant to be shared across threads. Consequently, if I start examining the contents of NSManagedObjectContextDidSaveNotification
on the main thread, I get crashes. Hmm ... so how does mergeChangesFromContextDidSaveNotification:
do it? Apparently, mergeChangesFromContextDidSaveNotification:
is specifically designed to work around the "don't share managed objects across threads" restriction.
The third option I thought of was to register for NSManagedObjectContextDidSaveNotification
on the background thread and while still on the background thread convert its contents into a special PriceChangeNotification
containing object IDs instead of managed objects. On the main thread, I could convert the object IDs back into managed objects. This approach would still require the to-one Price
relationship so that changes in prices are reflected as changes to Price
managed objects.
I based my fourth option on TechZen's suggestion to override the price setter in the Product
managed object. Rather than use a to-one relationship to force Core Data to generate the notifications I needed, I went back to using a price
attribute. In my setPrice
method, I post a custom PriceChangeNotification
. This notification is received on the background thread and is used to construct a set of Product
objects with price changes. After the background thread saves its context, it posts a custom PricesDidChangeNotification
which includes the object IDs of all Product
objects whose prices have changed. This notification can be safely transferred to the main thread and examined because it uses object IDs instead of managed objects themselves. On the main thread I can fetch the Product
objects referenced by those object IDs and queue an operation to perform the lengthy "price change" calculation on a new background thread.
Are you using an NSArrayController
or some other controller? Presumably you need some way for the user to interact with the model. It's this point of interaction that give a nice hook for this type of update call. Perhaps the appropriate strategy is to observe the relevant properties of the array controller's arrangedObjects
.
精彩评论