Alternative to overriding didChangeValueForKey: on NSManagedObject - ios

I have a managed object which acts as a playlist, it has a to-many relationship with the playlist items. There can be multiple playlists, but only one "active" playlist. The active playlist is indicated by a boolean attribute on the managed object.
I have the number of items in the active playlist displayed as the badge on a tab bar item. The view controller that the tab bar item represents is listening for a specific notification which is fired when the contents of the active playlist are updated.
I have implemented this in what I feel is a clumsy way and would like to do it better. It does work at the moment but I'm not happy with it.
Currently, each playlist object, on awakeFromFetch, checks to see if it is the active one, and if so registers itself (using key value observing) as an observer for the key path which is the key for the relationship. When it observes a change, it fires the notification, which causes the tab bar item to update itself.
If the playlist loses or gains active status it stops or starts observing itself appropriately, so notifications are only fired from the active playlist.
I would like to drop all of the self-observing KVO code as I am concerned about the various entry and exit points and when to add and remove observers appropriately. It seems too dirty.
I would like to just override didChangeValueForKey:, check and send my notification there if necessary, then call the super implementation. But this is expressly forbidden in the documentation:
didChangeValueForKey:
Invoked to inform the receiver that the value of a given property has changed.
-(void)didChangeValueForKey:(NSString *)key
Parameters
key
The name of the property that changed. Discussion For more details, see
Key-Value Observing Programming Guide.
You must not override this method.
So, what can I do?

I've just read the same documentation, but if you look at the top of the NSManagedObject documentation, it actually says "You are strongly discouraged..."
I guess it all depends on your implementation details. For example, I do the following on a data model that I can modify locally, and sync with a server:
- (void)didChangeValueForKey:(NSString *)key
{
[super didChangeValueForKey:key]; // MUST CALL THIS!
if ([key isEqualToString:NSStringFromSelector(#selector(name))] ||
[key isEqualToString:NSStringFromSelector(#selector(text))] ||
[key isEqualToString:NSStringFromSelector(#selector(filename))]
)
{
self.lastModified = [NSDate date];
}
}
I'm not sure why this would be considered bad. It's just saying "Do what you normally do. In addition, I'd like to set another property that depends on that change."

What you want is Key Value Observing. You should be able to register for that specific key, and get notified when it changes. Check this out too: Using KVO to observe changes to a property on an object inside a collection in Objective-C

I have solved this by making a separate object (a singleton I am using to manage my core data stack) the observer instead. All of the self-observing complexity has now gone and I do not need to worry about adding or removing observers on awakeFromFetch and so on.

Related

Safe Key Value Observing of keypaths

I am trying to implement a safe key value observing on keypaths. Let's suppose that I have a data model object named person that have a workplace property. The workplace in turn have a address property that I wish to observe so I set up key value observing with the following call:
[person addObserver:theObserver
forKeyPath:#"workplace.address"
options:NSKeyValueObservingOptionNew
context:NULL];
This works fine until the person does not change workplace. As soon as this happens:
person.workplace = newWorkplace;
the KVC system crashes the application correctly saying that "oldAddress was deallocated while key value observers were still registered with it". (being oldAddress the address of the previous workplace).
Unfortunately I can not tweak the implementation of the class of 'person' object to notify the observer that workplace is going to go away. Are there any pattern to avoid this kind of crash? Maybe one can get some other notifications? How is the keypath being traversed in the case of KVC and do you have access to this chain?
Edit 2
After spending some more time with KVO, I found that in your case, you should be observing person.workplace.address instead of workplace.address. When you observe person.workplace.address, you achieve 2 things:
1) Since you owned the person object, you have absolute control over your object's life cycle. You can now removeObserver at a time of your own choosing.
2) When workplace object is changed, the KVO mechanism will "automagically" observe the new address of the new workplace. Of course, it will notify you of the new address.
Now, you can observe the address without fearing workplace being replaced at random time. This is one of the true hidden power of KVO. This allow subclasses to safely observe any superclass's objects without knowing about their life-cycles.
Edit 1
Best practice to remove an object as observer for some KVO property
The accepted answer in this thread best described your situation. You should not have observe the property address in the first place, since you have no control over the life-cycle of workplace. You have a design issue.
Original
You can observe the keyPath workplace on the person object. When this keyPath is invoked, you simply removeObserver for workplace.address.
[person addObserver:theObserver
forKeyPath:#"workplace"
options:[NSKeyValueObservingOptionNew]
context:NULL];

Remove all references to ManagedObjects belonging to a ManagedObjectContext

I'm looking to integrate iCloud with a Core-Data-managed SQLite database (only on iOS 7 and later). I've been reading Apple's guide on using Core Data with iCloud (https://developer.apple.com/library/ios/documentation/DataManagement/Conceptual/UsingCoreDataWithiCloudPG/UsingCoreDataWithiCloudPG.pdf).
To quote from the guide, "Core Data posts an NSPersistentStoreCoordinatorStoresWillChangeNotification notification. In your notification handler, you reset your managed object context and drop any references to existing managed objects."
Calling -reset on the MOC to reset it isn't the problem, the problem is the part where they say all references to managed objects need to be dropped. I understand why this needs to be done (because the persistent store is changing), what I don't know is how to do it.
All my Core Data work is handled by a singleton and I had originally thought of posting a notification, and listening classes could set all their managed objects to nil. First, this doesn't sound like a particularly good way of doing it. Secondly, I have a FetchedResultsController managing a tableView, the FetchedResultsController manages it's own managed objects, therefore, as far as I know, I can't set them to nil.
I'd be really grateful for any advice on what to do here.
Thanks in advance.
The way I handle situations like this is to post two notifications in my app: just before resetting, and just after resetting.
For example, I might post MYMainContextWillResetNotification, then reset the context, then post MYMainContextDidResetNotification.
Any controller receiving the will-reset notification should release its managed objects, but also store any information it will need to recover after the reset. Usually this will be one or more NSManagedObjectID objects. In some cases, you may not need to store anything, simply performing a fetch after the reset instead.
A typical method might look like this:
- (void)mainContextWillReset:(NSNotification *)notif
{
self->noteID = note.objectID;
}
This code supposes there is a controller for a single note object. When the reset is about to take place, the note's object identifier is stored in an instance variable.
The did-reset notification method retrieves the note.
- (void)mainContextDidReset:(NSNotification *)notif
{
note = [context existingObjectWithID:noteID error:NULL];
[self refreshViews];
}
This code uses existingObjectWithID:error:, but you could equally do a fetch.
With an NSFetchedResultsController, you would need to call performFetch: in the did-reset method, to refresh the objects.

Identifying old and new values on changed objects with NSManagedObjectContextWillSaveNotification

I am trying to track changes to objects in a core data context, tracking the name of properties that have changed along with the old and new values.
I've registered for NSManagedObjectContextWillSaveNotification to receive a notification when a save is about to occur, and can pull out the inserted/updated/deleted objects from the context... I can then see the changed values using .changedValues.
However, I am having difficulties retrieving the old values...
As an example, I have an object that tracks a position, and so one of the changes comes back with:
po [obj changedValues]
{
originX = 260;
originY = 180;
}
This gives me the new values for the properties that have changed on the object. To try and get the old values, I'm then using changedValuesForCurrentEvent, which according to the docs should return
"a dictionary containing the keys and old values of persistent
properties that have changed since the last posting of
NSManagedObjectContextObjectsDidChangeNotification"
However, when I try this, it is coming back empty...:
po [obj changedValuesForCurrentEvent]
{
}
How can I capture the old and new values?
You're mixing up your notifications. NSManagedObjectContextObjectsDidChangeNotification gets called any time you change values on a managed object, even though you haven't saved changes yet. NSManagedObjectContextWillSaveNotification gets called later on when you save. So the sequence is:
You change some attributes --> NSManagedObjectContextObjectsDidChangeNotification is posted, and you can use changedValuesForCurrentEvent to see what changed.
Later, you save changes. NSManagedObjectContextWillSaveNotification is posted. You can call changedValuesForCurrentEvent, but it's not helpful because it returns changes since the last did-change notification. There are no changes since the last did-change notification. If there were, you would have received another one. That method is documented to be useful on a did-change notification, not on a will-save notification.
If you want the old values and you want to get them when the will-save notification is posted, you have a couple of options:
Listen for NSManagedObjectContextObjectsDidChangeNotification. Cache information about changes in some collection object (probably NSDictionary). Then when NSManagedObjectContextWillSaveNotification happens, look up those changes, process them, and clear the change cache. OR...
When you get NSManagedObjectContextWillSaveNotification, create a second local managed object context. Since this is a will save notification, you can still fetch the old values. So, fetch each object that's getting saved and compare the before and after values to see what's different.
Although this question is 4 years old, Eddie's answer was very helpful. I made a little change to his answer. All the credits goes to him.
object.setValuesForKeys(object.committedValues(forKeys: object.changedValues().map { $0.key }))
I know this question is old, but there is a better way than the accepted answer. You can access the previous values via committedValues(forKeys:) in combination with changedValues(). There is no need to handle NSManagedObjectContextObjectsDidChangeNotification or to create another managed object context.
Here is some sample code that I use:
// For some reason, the Swift compiler chokes on the type of object.changedValues().keys.
// It should be of type [String], but it complains that it is of type `Dictionary<String, Any>.Keys`
// which is useless. Ah, the joys of Apple programming...
// Work around that like so:
var changedKeys = [String]()
for (key, _) in object.changedValues() {
changedKeys.append(key)
}
let oldData = object.committedValues(forKeys: changedKeys)
Sounds like you should call "changedValuesForCurrentEvent" only when you receive your "NSManagedObjectContextWillSaveNotification" notification.
And if "changedValuesForCurrentEvent" still returns a null dictionary or object, check to see if the notification had anything useful in it's "userInfo" dictionary itself. It also may be that there has not been a NSManagedObjectContextObjectsDidChangeNotification" posted, like you posted from the docs up there.

How to avoid calling a method multiple times when the method takes long time to complete

There are several view controllers in my app where I need to sync the local contents with server using a method running in a background thread. Sometimes I need to insert data to my database on server if user has created any. The approach I am using here is to set a flag(something like isSynced = NO) on objects that I need to sync with server (there objects are in Core Data). When the syncing is complete my method will get rid of the flag(e.g. isSynced = YES) so it won't be sent again next time.
Now the problems is that the syncing method takes very long to complete(1 or 2seconds.). If now user pops out this particular view controller and swiftly comes back the previous call is still in progress and next one will be kicked off. The consequence is that there might be duplication in database.
My approach now is the make the syncing method to be called by a Singleton object:
#property (nonatomic) BOOL isSyncing;
//every time before syncing. check if object is available for syncing
if (!isSyncing) {
isSyncing = YES;
// sync server
// when complete
isSyncing = NO;
// post notification to view controller to reload table
} else {
// cancel because previous call is not finished
}
My concern is that if the call is cancelled my view controller will not be able to receive the notification is waiting for. I can fix this by posting another notification in the event of cancelation. I am wondering if this is the right to do this because I think that this problem should be pretty common in iOS development and there should be a standard way to deal with it
Your singleton approach may not be necessary. I don't see the harm in sending a database insert for each new object. You will still need to ensure each object is synched. That is, update the "isSynched" flag. Keep each object that needs to be synced in a "need to synch" list.
Then, update the "isSynced" flag by performing a background query on the database to check if the object exits. Then, use the result of the query to set the isSynched flag.
If the query result indicates the object is not in the database you then resend the object and leave it's "isSynced" flag set to NO.
If the query result indicates the object is in the database, set the "isSynced" flag to YES and remove it from your "need to synch" list.
An approach for preventing duplicate database entries is to make a unique key. For example, tag each with a hash based on the time and date. Then configure the table to ensure each key is unique.

Sending NSNotifications to all objects of a class

I have an object that can be selected by a user click. With the current requirements of the app, at any time, there is no more than one of these items selected at any point during app execution.
I implemented a mechanism to enforce this, as follows:
Each of these objects has a unique identifier as a property.
When each object is created, it subscribes to the NSNotificationCenter listening for the MY_OBJECT_SELECTED notification.
When each object is selected, it posts the MY_OBJECT_SELECTED notification, with its unique Id as part of the userInfo dictionary.
Then, when each object receives the notification, it checks to see if its id is the same as the one in the userInfo. If it is, it does nothing, but if it isn't, it sets itself to unselected.
Is this a decent approach to the problem? If not, how would you do it?
It is a decent way of doing it, although it is not very efficient. The more objects you have, the more time you spend comparing IDs. The easiest way is to store your object pointers and IDs in a map table (or similar) and remember the last selected object. Whenever you select a new object, you clear the selection flag of the last selected object, then look up the new object and set its selection flag. It requires you to keep a collection of your objects, though.
The time required to update selections with this approach is independent of the number of objects you have.
If the object is spread all over the app,i.e. if it is a member in various classes. You can have a global object of same type and assign it to only that object which has been touched. In steps it will be like:
Have a global variable of object type.
At any object touch assign globalObject = currentObject;
do all operations on globalObject throughout app like calling methods and modifying object members(have a check for nil to ensure safety).
Reassign to different object with new touch.

Resources