Set a lastModificationDate attribute after NSManagedObjectsDidChangeNotification creates an infinite loop - ios

I added a lastModifiedDate attribute to all my entities to avoid duplicates when syncing the UIManagedDocument with iCloud, something that I found it can happen if I create new entities with an offline device (iPad) and, at the same time, I create the same entities using another online device (iPhone).
I wanted to set this attribute whenever an object changes so I subscribed for NSManagedObjectContextObjectsDidChangeNotification. The code I wrote to set the lastModifiedDate creates an infinite loop because by setting the lastModificationDate attribute it creates a change that will be notified again by NSManagedObjectContextObjectsDidChangeNotification and so on...
Is it possible to fix it? Is there a better way to accomplish my goal? Should I subclass managedObjectContext and override willSave:?
//At init...
[[NSNotificationCenter defaultCenter] addObserver:applicationDatabase
selector:#selector(objectsDidChange:)
name:NSManagedObjectContextObjectsDidChangeNotification
object:applicationDatabase.managedDocument.managedObjectContext];
(void) objectsDidChange: (NSNotification*) note
{
// creates an infinite loop
NSDate *dateOfTheLastModification = [NSDate date];
NSMutableArray *userInfoKeys = [[note.userInfo allKeys] mutableCopy];
for(int i=0; i< userInfoKeys.count;i++){
NSString *key = [userInfoKeys objectAtIndex:i];
if([key isEqualToString:#"managedObjectContext"]){
[userInfoKeys removeObject:key];
}
}
for(NSString *key in userInfoKeys){
NSArray *detail = [note.userInfo objectForKey:key];
for (id object in detail){
[object setValue:dateOfTheLastModification forKey:#"lastModifiedDate"];
}
}

To avoid the infinite loop, you could set the last modification date using the
primitive accessor:
[object setPrimitiveValue:dateOfTheLastModification forKey:#"lastModifiedDate"];
because that does not fire another "change" notification. But that also implies that
no observers will see the change.
Overriding willSave in the managed object subclass would suffer from the same problem.
The Apple documentation for willSave states:
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 you should register for NSManagedObjectContextWillSaveNotification instead,
and set the timestamp on all updated and inserted objects in the managed object
context. The registered method could look like this:
-(void)contextWillSave:(NSNotification *)notify
{
NSManagedObjectContext *context = [notify object];
NSDate *dateOfTheLastModification = [NSDate date];
for (NSManagedObject *obj in [context insertedObjects]) {
[obj setValue:dateOfTheLastModification forKey:#"lastModifiedDate"];
}
for (NSManagedObject *obj in [context updatedObjects]) {
[obj setValue:dateOfTheLastModification forKey:#"lastModifiedDate"];
}
}
This assumes that all your entities have a lastModifiedDate attribute, otherwise
you have to check the class of the objects.

Related

MagicalRecord using

I'm new to CoreData and MR. Trying to save some entities and read them after.
saving:
Events *newEvent = [Events MR_createEntity];
newEvent.title = #"qwe";
newEvent.date = [NSDate date];
[[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreWithCompletion:^(BOOL contextDidSave, NSError * _Nullable error) {}];
reading:
NSMutableArray *events = [NSMutableArray arrayWithArray:[Events MR_findAll]];
NSLog(#"%#",events);
as result I'm getting "data: < fault >"
if I add private context like:
NSManagedObjectContext *context = [NSManagedObjectContext MR_newPrivateQueueContext];
NSMutableArray *events = [NSMutableArray arrayWithArray:[Events MR_findAllInContext:context]];
my app crashes with error reason: '+entityForName: nil is not a legal NSPersistentStoreCoordinator for searching for entity name 'Events''
Can someone show me code working for my task
You don't need to add any private context for this (if you don't need it for other reasons, obvious). The "data: <fault>" is part of the iOS. Core Data doesn't extract the information of an object if they are not directly accessed, this is a good choice for performance reasons. So, if you want to print in console your array, you have to cycle it and print every single element extracting it from the array.
for (Event *event in [Events MR_findAll]) {
NSLog(#"Event name : %#", event.name)
}
This should work good.
PS: A little advice, use singular names for your entities because they represent a single object, a single class. Don't think at them as tables because they are not.

Objective C - How To Keep Reference To Multiple Objects With Keys Just Like How NSMutableDictionary Works

I just learned how to make use of KVO, but only the basics. What I need to achieve is something like this:
I have a delegate call that passes a Speaker object.
- (void)onSpeakerFound:(Speaker *)speaker
Once I receive this Speaker in the UI part, from there I will assign observers for this object.
But, this is just for one speaker. What if I have multiple speakers to keep track of. I need to assign observers separately for those speakers and then at the same time I wish to keep their references for further updates to the values.
Each speaker could be updated from time to time. So when I notice that there is a change that happened on a speaker, I wish to access the reference to that speaker and update the values just like how NSMutableDictionary works.
NSMutableDictionary makes a copy of an object set to it so it will be a difference object if I get it again from the dictionary.
So, is there a class that allows me to keep track of an object by just keeping a reference only to that object without making a copy of it?
EDIT: A Test Made To Verify That When An Instantiated Object is Set in an NSMutableDictionary, The Instantiated Object is not referenced with the one set inside NSMutableDictionary.
- (void)viewDidLoad
{
[super viewDidLoad];
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
NSString *obj = #"initial value";
NSString *key = #"key";
[dict setObject:obj forKey:key];
NSLog(#"Object is now %#", [dict objectForKey:key]);
obj = #"changed value";
NSLog(#"Object is now %#", [dict objectForKey:key]);
}
Log:
2016-07-26 21:04:58.759 AutoLayoutTest[49723:2144268] Object is now initial value
2016-07-26 21:04:58.761 AutoLayoutTest[49723:2144268] Object is now initial value
NSMutableDictionary makes a copy of an object set to it...
That is not correct; it will add a reference to the object. It will be the same object referenced inside and outside the Objective-C collection.
So, is there a class that allows me to keep track of an object...?
Probably NSMutableSet if you just want a list of the objects. That will take care that you have a unique reference to each object, however you need to implement the methods hash and isEqual on those objects so they behave correctly. Otherwise NSMutableDictionary if you want fast look-up by key.
-try this one
- (void)viewDidLoad
{
[super viewDidLoad];
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
NSString *obj = #"initial value";
NSString *key = #"key";
[dict setObject:obj forKey:key];
NSLog(#"Object is now %#", [dict objectForKey:key]);
obj = #"changed value";
[dict setObject:obj forKey:Key];
NSLog(#"Object is now %#", [dict objectForKey:key]);
}

indexPathForObject returns nil

I am passing an object from my UITableViewController to a singleton, then back to my UITableViewController through a notification. The UITableViewController uses a NSFetchedResultsController. It then want the indexPath of the Object in the fetchedResultsController.
Object *obj = (Object *)notification.object;
NSIndexPath *index = [self.fetchedResultsController indexPathForObject:obj];
I set the notification in the AFNetworking/AFDownloadRequestOperation progress block:
- (void)downloadObject:(Object *)object
{
........
[operation1 setProgressiveDownloadProgressBlock:^(AFDownloadRequestOperation *operation, NSInteger bytesRead, long long totalBytesRead, long long totalBytesExpected, long long totalBytesReadForFile, long long totalBytesExpectedToReadForFile) {
////// Sending Notification
////// Try to send notification only on significant download
totalBytesRead = ((totalBytesRead *100)/totalBytesExpectedToReadForFile);
NSLog(#"TOTAL BYTES IN PROGRESS BLOCK: %llu",totalBytesRead);
NSMutableDictionary *userDictionary = [[NSMutableDictionary alloc]init];
[userDictionary setObject:[NSNumber numberWithLongLong:totalBytesRead] forKey:#"progress"];
[userDictionary setObject:[NSNumber numberWithLongLong:totalBytesExpectedToReadForFile] forKey:#"totalBytes"];
[[NSNotificationCenter defaultCenter] postNotificationName:[NSString stringWithFormat:#"DownloadProgress%#",object.uniqueID] object:object userInfo:userDictionary];
}
There is only one object in [self.fetchedResultsController fetchedObjects]; and it matches the object the passed back in the notification. The index is always nil, but the fetchedResultsController is not nil, and the [self.fetchedResultsController fetchedObjects] returns the correct object. Why does indexPathForObject nil?
I had the same problem. In the configuration of my NSFetchedResultsController the sectionNameKeyPath was set to refer to an (optional) related object.
It turned out that this relation must be fulfilled (that is the related object must not be nil).
After ensuring, that every relation of the sectionNameKeyPath is fulfilled, the indexPathForObject method worked again as expected.
Looks to me like you are attempting to locate an object in a table view from the notification object...
You should be calling the index path method on your table view object, not your notification object.
Try matching your local variable:
Object *obj = (Object *)notification.object;
with:
[self.fetchedResultsController fetchedObjects];
(if it is the only one that should be relatively easy)...
NSArray *arrayFetchedObjects = [self.fetchedResultsController fetchedObjects];
Object *tableObject = [arrayObjects lastObject];
(if there is more than one then you might need to filter the arrayFetchedObjects but that is another question)
then locating the position of that fetched object in the table view:
NSIndexPath *indexPath = [self.fetchedResultsController indexPathForObject:tableObject];

Trying to iterate through an obj-c object properties and copy them to another object

I have a custom object saved to NSUserDefaults. I don't use Core Data as it is way to much for my one object. I am trying to create a method that takes a new object of this type and combine it with the data in a saved object of this type.
Here is what I have so far, but it is erroring out because some objects are of different types (BOOL, NSNumber, NSString, NSDictionary, etc..).
if (request) {
unsigned int numberOfProperties = 0;
objc_property_t *propertyArray = class_copyPropertyList(
[AmbulanceRequest class], &numberOfProperties);
for (NSUInteger i = 0; i < numberOfProperties; i++)
{
objc_property_t property = propertyArray[i];
NSString *name = [[NSString alloc] initWithUTF8String:property_getName(property)];
NSString *attributesString = [[NSString alloc] initWithUTF8String:
property_getAttributes(property)];
if( nil == [request valueForKey:name]) { <--- error here BOOL cannot be nil
[request setValue:[savedRequest valueForKey:name] forKey:name];
}
}
}
Error I am getting is: [NSKeyedUnarchiver initForReadingWithData:]: data is NULL
UDPATE:
I am just using decoder and encoder. When encoding and decoding in general I never have issues. Just in the code above. For example, here is one property that is throwing the error.
Of course these are in the appropriate encodeWithCoder and initWithCoder methods.
#property (nonatomic, assign) BOOL * trip;
self.roundTrip = [decoder decodeBoolForKey:#"trip"];
[encoder encodeBool:self.roundTrip forKey:#"trip"];
//// UPDATE 2: //////
Here is the full code of my method. As you can see the object is pulled back and decoded way before any of this iterating through the properties happens. I have two BooRequests the old one and a new one. I am trying to take the properties of the old one and save them to the new one if the new one doesn't have those same properties.
+(void) saveRequest:(BooRequest*)request {
// If current request then we need to save off the unSetPings
BooRequest *savedRequest = [NSKeyedUnarchiver unarchiveObjectWithData:[[NSUserDefaults standardUserDefaults]
objectForKey:kCurrentActiveRequest]];
if(request){
unsigned int numberOfProperties = 0;
objc_property_t *propertyArray = class_copyPropertyList([BooRequest class], &numberOfProperties);
#warning YO!!! this FOR LOOP is *NOT* done and probably doesn't even work!!!
for (NSUInteger i = 0; i < numberOfProperties; i++)
{
objc_property_t property = propertyArray[i];
NSString *name = [[NSString alloc] initWithUTF8String:property_getName(property)];
NSString *attributesString = [[NSString alloc] initWithUTF8String:property_getAttributes(property)];
if ([[[NSUserDefaults standardUserDefaults] dictionaryRepresentation].allKeys containsObject:#"keyForNonMandatoryObject"]) {
id thing = [request valueForKey:name];
if(NULL == [request valueForKey:name] || nil == thing){
[request setValue:[savedRequest valueForKey:name] forKey:name];
}
}
if(savedRequest) {
request.unSentPings = [[NSMutableArray alloc] initWithArray:savedRequest.unSentPings];
}
request.nextButtonToShowIndex = savedRequest.nextButtonToShowIndex;
} // end for loop
} else {
STPingSender *sender = [[STPingSender alloc] init];
BooRequest *req = [BooRequest currentRequest];
if(req && req.unSentPings && [req.unSentPings count] > 0){
[sender sendToServer:((BooRequest*)[BooRequest currentRequest]).unSentPings];
}
}
[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:request]
forKey:kCurrentActiveRequest];
[[NSUserDefaults standardUserDefaults] synchronize];
}
Here is my ultimate goal I have a BooRequest that is saved locally from the server. I make changes to it locally. 90% of the time their are not updates from the server which I need to update locally. But when there are I need the new local data merged with the new server data. But the local data I add is only used locally, the server never uses this additional data. It only needs to be merged locally. The reason I am iterating is that I don't want to have to update this method everytime this Objective-c class gets a new property added to it. I want some dynamic code that just says iterate through the properties and when you find the properties that match on the old object and new object, copy the old object properties into the new ones.
id dataObject = [[NSUserDefaults standardUserDefaults] objectForKey:kCurrentActiveRequest];
BooRequest *savedRequest = [NSKeyedUnarchiver unarchiveObjectWithData:dataObject];
when dataObject is nil, which means there is no such key in user defaults, the second line will emit the [NSKeyedUnarchiver initForReadingWithData:]: data is NULL error, in this case, you should not unarchive it, if the new BooRequest object is not nil, you can save the new object to user default directly.
id thing = [request valueForKey:name];
For properties that are of primitive type such as BOOL or NSInteger, the thing is NO and 0 respectively when you don't set the value of the properties, which means the if(NULL == [request valueForKey:name] || nil == thing) check is always false and the new request object is not updated from the old one. I suggest you to change those properties to NSObject type, so the check will pass.
#property (nonatomic, strong) NSNumber* boolProperty;
#property (nonatomic, strong) NSNumber *integerProperty;
Hope that helps.
I'm not sure why you are trying to store a custom object in NSUserDefaults in the first place... that is usually more pain than it is worth.
Why not just create an NSDictionary and save that to NSUserDefaults? Then your BooRequest class can init from dictionary as well as merge from dictionary. You can also create a method -saveToUserDefaults method in BooRequest that saves it back when completed. And depending on what BooRequest actually does you may not need a custom subclass if all you are doing is storing some key-value pairs.
It would be helpful to see what your local data looks like, and what your remote data looks like, and then the desired outcome.
You shouldn't blindly iterate through all the object properties but choose those worth being saved.
As said in another answer, don't use NSUserDefaults for this. If you don't want to use Core Data I recommend serializing Property Lists. As a bonus you'll be able to inspect/manually edit your object values.

Modifying sort key in awakeFromFetch doesn't update sort order

I have a Core Data object that has a property "completed" and also a time to reset this value:
#property (nonatomic) BOOL completed;
#property (nonatomic, retain) NSDate * next_reset;
I fetch these objects with a NSFetchedResultsController, sorting on the "completed" key:
NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc] initWithKey:#"completed" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor1,nil];
The wrinkle: I reset "completed" on fetch:
- (void)awakeFromFetch
{
[super awakeFromFetch];
if (!self.auto_reset || !self.completed)
{
return;
}
NSDate *now = [NSDate dateWithTimeIntervalSinceNow:0];
if ([now compare:self.next_reset] == NSOrderedDescending)
{
self.last_reset = now;
self.completed = NO;
}
}
My problem is that this modification is not reflected in the sorting of the fetched results - I get completed items mixed in with uncompleted ones. When I complete an item "live" the sorting does update as I expect.
According to the docs, in awakeFromFetch, Core Data is not monitoring changes made to the object. I do see that even a [self.managedObjectContext save:nil]; does not save my changes made here. I've tried calling -willChangeValueForKey/-didChangeValueForKey before I modify "completed" but this doesn't change things.
Where can I do these kinds of updates to the object so that Core Data properly sees them? Obviously I don't want to do it anywhere but the model (certainly not in my controller that is fetching these objects!) but I don't see any other places to put this update code.
I at least can get this to sort of work with this code:
NSDate *now = [NSDate dateWithTimeIntervalSinceNow:0];
NSTimeInterval interval_to_next_reset = [self.next_reset timeIntervalSinceDate:now];
if (interval_to_next_reset < 0.01)
{
interval_to_next_reset = 0.01;
}
[self performSelector:#selector(resetCompleted) withObject:nil afterDelay:interval_to_next_reset];
}
- (void)resetCompleted
{
self.last_reset = [NSDate dateWithTimeIntervalSinceNow:0];
self.completed = NO;
}
but this causes the table view to visibly rearrange itself immediately after being displayed, which isn't ideal.
I ended up solving this problem by not doing it this way.
Instead of writing this code in -awakeFromFetch (which clearly isn't meant to be used this way) I added code in my app delegate's -didBecomeActive (& friends) to call a class method +processQuestsNeedingReset which updates all of these at once. That update is reflected in the table view.

Resources