I'm using core data to persist my data model which is very simple, three entities like this: A<-->>B<-->>C. When something change in my model I need to perform some activity. To do so, I'm using listening to NSManagedObjectContextObjectsDidChangeNotification, using the following code in viewDidLoad:
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(updateReminder:)
name:NSManagedObjectContextObjectsDidChangeNotification
object:self.managedObjectContext];
In the method updateReminder I do what needed.
The problem is that when the notification execute the changes aren't persisted. If new entity was created not get saved, if something is changed the change don`t get saved.
Any ideas?
EDIT:
The code of updateReminder is as follows:
- (void)updateReminder:(NSNotification *)notification
{
A *entityAUpdated = [[notification userInfo] objectForKey:NSUpdatedObjectsKey];
if (entityAUpdated) {
...Do something when entityA is updated.
}
A *entityADeleted = [[notification userInfo] objectForKey:NSDeletedObjectsKey];
if (entityADeleted) {
...Do something when entityA is Deleted.
}
A *entityAInserted = [[notification userInfo] objectForKey:NSInsertedObjectsKey];
if (entityAInserted) {
...Do something when entityA is Inserted
}
}
Related
I have a model with this one to many relationShip:
Order -->> LineItem
I display LineItems in UITableViewCells:
I use UIPickerView for changing quantity of LineItems.
GOAL=> by changing picker value, subTotal be recalculated again.
the problem is here by updating lineItem, NSFetchedResultsController Delegate doesn't call (where I can reconfigure the cell again and display updated data). but when I update Order e.g set it as completed NSFetchedResultsController Delegate methods will be called.
why by updating lineItem doesn't affect delegates methods to be called?
I use magicalRecord and here is how I get NSFetchedResultsController
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
else
{
_fetchedResultsController = [Order fetchAllSortedBy:#"orderDate" ascending:YES withPredicate:nil groupBy:nil delegate:self];
}
return _fetchedResultsController;
}
the way I setup table view:
ConfigureCellBlock configureCell = ^(OrderDetailsCell *cell, LineItem *lineItem)
{
[cell configureForLineItem:lineItem];
};
//set fetchedresults controller delegate
Order *order = [[self.fetchedResultsController fetchedObjects] lastObject];
NSArray *lineItems = [order.lineItems allObjects];
self.ordersDataSource = [[ArrayDataSource alloc] initWithItems:lineItems cellIdentifier:#"lineItemCell" configureCellBlock:configureCell];
self.tableView.dataSource = self.ordersDataSource;
configuring cell:
- (void)configureForLineItem:(LineItem *)lineItem
{
self.menuItemName.text = lineItem.menuItemName;
self.price.text = [lineItem.unitPrice stringValue];
self.quantity.text = [lineItem.quantity stringValue];
self.totalPrice.text = [lineItem.totalPrice stringValue];
self.pickerController.model = lineItem;
self.picker.delegate = self.pickerController;
self.picker.dataSource = self.pickerController;
[self.picker setSelectedNumber:lineItem.quantity];
}
does fetching obj1 then updating obj3 cause the NSFRC delegate methods to be called?
The FRC will only observe changes to the objects that it is directly interested in, not any of the objects that they are related to.
You should configure your own observation, either directly with KVO or to the context being saved, and use that to trigger a UI refresh.
I have developed a project, where a user draws a image on a canvas, I store it in the file using CoreData, I have one-to-many relationship called folder-to-files. So here all are images. I retrive the images from files , resize according to my table cell height and show it on a table. Once it is shown, I want to cache the images.
I also have some labels on the folder cell, which give me some info regarding my files, which I update on fly.
I also swipe the cells to mark it complete and move it to the bottom the cell.
I also show same file images in different Views depending on how user queries it.
I want to know the best method for this, I read through the web, their are many methods, GCD, NSOperationQueues and many more.
Which method will be best suited for me.
I want to show some code
- (UITableViewCell *)tableView:(FMMoveTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
FolderCell *tableCell = (FolderCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (tableCell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"FolderCell" owner:self options:nil];
tableCell = [nib objectAtIndex:0];
}
NSMutableArray *categoryArray = [[self.controller fetchedObjects]mutableCopy];
Folder *category = [categoryArray objectAtIndex:[indexPath row]];
[tableCell configureCellWithNote:category]; //This function is written in my FolderCell.m function
}
return tableCell;
}
-(void)configureCellWithNote:(Folder *)category
{
self.category = category;
UIImage *image1 = [UIImage imageWithData:category.noteImage];
CGSize newSize;
if(image1.size.width == 620 && image1.size.height == 200)
{
newSize = CGSizeMake(300, 97);
}
UIGraphicsBeginImageContextWithOptions(newSize, NO, 0.0);
[image1 drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
self.notesImage.image = newImage;
}
So what is happening here is that configureCellWithNote is taking lot of time, because it is resizing images. Please help me out in deciding how can this performance issue be solved.
Regards
Rajit
If you simply want to shuffle the resize operation to a background thread, you could do something like this:
- (void)configureCellWithNote:(Folder *)category
{
self.category = category;
UIImage *image1 = [UIImage imageWithData:category.noteImage];
CGSize newSize;
if(image1.size.width == 620 && image1.size.height == 200)
{
newSize = CGSizeMake(300, 97);
}
dispatch_async(dispatch_get_global_queue(0,0), ^{
UIGraphicsBeginImageContextWithOptions(newSize, NO, 0.0);
[image1 drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
self.notesImage.image = newImage;
});
});
}
If you want to cache the results, then the trick will be to come up with a good cache key. Unfortunately, it's hard to tell from what you've posted what would make a good cache key. Certainly it will need to include the size, but it'll also need to include something that ties it back to the category. I suppose if nothing else you could use the NSManagedObjectID for the category, but I think that'll be specific to each managed object context you have. Assuming there was a property on Folder called uniqueName a caching implementation might look like this:
- (UIImage*)imageForCategory: (Folder*)category atSize: (CGSize)size
{
// A shared (i.e. global, but scoped to this function) cache
static NSCache* imageCache = nil;
// The following initializes the cache once, and only once
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
imageCache = [[NSCache alloc] init];
});
// Generate a cache key sufficient to uniquely identify the image we're looking for
NSString* cacheKey = [NSString stringWithFormat: #"%#|%#", category.uniqueName, NSStringFromSize((NSSize)size)];
// Try fetching any existing image for that key from the cache.
UIImage* img = [imageCache objectForKey: cacheKey];
// If we don't find a pre-existing one, create one
if (!img)
{
// Your original code for creating a resized image...
UIGraphicsBeginImageContextWithOptions(size, NO, 0.0);
UIImage* image1 = [UIImage imageWithData:category.noteImage];
[image1 drawInRect:CGRectMake(0,0,size.width,size.height)];
img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// Now add the newly-created image to the cache
[imageCache setObject: img forKey: cacheKey];
}
// Return the image
return img;
}
Is it possible to change attributes of managed objects in NSManagedObjectContextObjectsDidChangeNotification handler without firing the handler again?
I get the data from our server and RestKit maps the data into Core Data. I have to change some attributes after the data arrives in my database.
Thanks for help.
Edit:
This is my code. The handleDidChangeNotificationmethod is called in a cycle:
- (void)addMyObserver
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(handleDidChangeNotification:)
name:NSManagedObjectContextObjectsDidChangeNotification
object:self.objectManager.managedObjectStore.mainQueueManagedObjectContext];
}
- (void)handleDidChangeNotification:(NSNotification *)notification
{
NSSet *updatedObjects = [[notification userInfo] objectForKey:NSUpdatedObjectsKey];
NSSet *deletedObjects = [[notification userInfo] objectForKey:NSDeletedObjectsKey];
NSSet *insertedObjects = [[notification userInfo] objectForKey:NSInsertedObjectsKey];
// modifiedObjects with store entity:
NSSet *modifiedObjects = [updatedObjects setByAddingObjectsFromSet:insertedObjects];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"SELF isKindOfClass: %#", [MyStore class]];
NSSet *modifiedStoreObjects = [modifiedObjects filteredSetUsingPredicate:predicate];
if (modifiedStoreObjects.count > 0)
{
[modifiedStoreObjects enumerateObjectsUsingBlock:^(MyStore *store, BOOL *stop)
{
store.distanceValue = 1000;
}];
}
}
To modify a Core Data object without firing the change notifications, you can use the
primitive accessor methods, e.g.
[store setPrimitiveValue:#1000 forKey:#"distanceValue"];
(Note that an object value is required here, a scalar value does not work.)
But you should consider carefully if there are not any unwanted side effects, because
other listeners will also not be notified about the changed value.
Another possible solution might be to check if the attribute has to be changed at all,
and modify only if necessary.
The following code is off the top of my head, not tested, but this is how I would go.
#interface MyStoreCoordinator () {
bool _changingValue;
}
#end
#implementation MyStoreCoordinator
- (void)addMyObserver
{
_changingValue = NO;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(handleDidChangeNotification:)
name:NSManagedObjectContextObjectsDidChangeNotification
object:self.objectManager.managedObjectStore.mainQueueManagedObjectContext];
}
- (void)handleDidChangeNotification:(NSNotification *)notification
{
if (_changingValue)
{
return;
}
NSSet *updatedObjects = [[notification userInfo] objectForKey:NSUpdatedObjectsKey];
NSSet *deletedObjects = [[notification userInfo] objectForKey:NSDeletedObjectsKey];
NSSet *insertedObjects = [[notification userInfo] objectForKey:NSInsertedObjectsKey];
// modifiedObjects with store entity:
NSSet *modifiedObjects = [updatedObjects setByAddingObjectsFromSet:insertedObjects];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"SELF isKindOfClass: %#", [MyStore class]];
NSSet *modifiedStoreObjects = [modifiedObjects filteredSetUsingPredicate:predicate];
if (modifiedStoreObjects.count > 0)
{
[modifiedStoreObjects enumerateObjectsUsingBlock:^(MyStore *store, BOOL *stop)
{
_changingValue = YES;
store.distanceValue = 1000;
_changingValue = NO;
}];
}
}
Example how to should look subclass of NSManagedObject. YourPropertyName - is your class
#implementation YourPropertyName
#dynamic stringValueOfYourProperty;//for example
-(void)setStringValueOfYourProperty:(NSString *) _stringValueOfYourProperty
{
[self willChangeValueForKey:#"stringValueOfYourProperty"];
[self setPrimitiveValue: _stringValueOfYourProperty forKey:#"stringValueOfYourProperty"];
[self didChangeValueForKey:#"stringValueOfYourProperty"];
}
Then just use setStringValueOfYourProperty anywhere in code.
One workaround I successfully use is (I haven't notice side effects yet. Please let me know if I'm wrong).
In the didChange CoreData notification callback, I save in a global array all the updated objects.
In the willSave CordData notification callback, I modify the object saved at point 1
The didChange callback will be called again and the willSave will be automatically skipped
didSave CoreData notification callback if you want!!
I am saving the latest internet request of my tableviewdata in an (core data) entity, but have problems with error exceptions about "faults".
I have two methods 'loadData' which gets the latest 'ordersitems' that will be loaded in my tableview AND 'loadThumbnails' which will try to cache the thumbnail into the core data entity.
The problem occurs when the managedobject gets deleted and the thumbnail method still tries to access it. Though i made a variable stopThumbnails to stop the loadThumbnails method, the problem keeps occurring.
What is the proper iOS 6 way to lazyload the images and save them to coredata but check if the object has not been deleted? i found this Core Data multi thread application which was useful but my newbie understanding of core data is still limited and i have problems writing code. I read the apple docs about http://developer.apple.com/library/ios/#documentation/cocoa/conceptual/coredata/Articles/cdConcurrency.html but it was hard to understand completely.
I want at least my http request to load asychronous (but preferably as much as possible) i came up with the following:
-(void)viewdidload
{
NSFetchRequest *fetchReq = [NSFetchRequest fetchRequestWithEntityName:#"OrderItems"];
fetchReq.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES]];
self.data = [self.managedObjectContext executeFetchRequest:fetchReq error:nil];
MYFILTER = #"filter=companyX";
[self loadData];
}
-(void)loadData
{
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//json request from url
NSDictionary *reqData = myOrderJSONRequest(MYFILTER);
dispatch_async( dispatch_get_main_queue(), ^{
if(reqData!=NULL && reqData!=nil)
{
//request successful so delete all items from entity before inserting new ones
stopThumbnails = YES;
for(int i=self.data.count-1;i>=0;i--)
{
[self.managedObjectContext deleteObject:[self.data objectAtIndex:i]];
}
[self.managedObjectContext save:nil];
if(reqData.count>0)
{
//insert latest updates
for (NSDictionary *row in reqData){
OrderItem *item = [NSEntityDescription insertNewObjectForEntityForName:#"OrderItem" inManagedObjectContext:self.managedObjectContext];
item.order_id = [NSNumber numberWithInt:[[row objectForKey:#"order_id"] intValue]];
item.description = [row objectForKey:#"description"];
item.thumbnail_url = [row objectForKey:#"thumbnail_url"];
}
[self.managedObjectContext save:nil];
}
NSFetchRequest *fetchReq = [NSFetchRequest fetchRequestWithEntityName:#"OrderItems"];
fetchReq.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES]];
self.data = [self.managedObjectContext executeFetchRequest:fetchReq error:nil];
[TableView reloadData];
//LOAD THUMBNAILS ASYNCHRONOUS
stopThumbnails = NO;
[self loadThumbnails];
}
else{
//NO INTERNET
}
});
});
}
-(void)loadThumbnails
{
if(!loadingThumbnails)
{
loadingThumbnails = YES;
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i=0;i<self.data.count; i++) {
if(!stopThumbnails)
{
OrderItem *item = [self.data objectAtIndex:i];
if(item.thumbnail==NULL)
{
//ASYNCHRONOUS IMAGE REQUEST
NSURL *image_url = [NSURL URLWithString:item.thumbnail_url];
NSData *image_data = [NSData dataWithContentsOfURL:image_url];
dispatch_async( dispatch_get_main_queue(), ^{
if(image_data!=nil && image_data!=NULL && !stopThumbnails)
{
//IMAGE REQUEST SUCCESSFUL
item.thumbnail = image_data;
[self.managedObjectContext save:nil];
//RELOAD AFFECTED TABLEVIEWCELL
NSIndexPath* rowToReload = [NSIndexPath indexPathForRow:i inSection:0];
NSArray* rowsToReload = [NSArray arrayWithObjects:rowToReload, nil];
[TableView reloadRowsAtIndexPaths:rowsToReload withRowAnimation:UITableViewRowAnimationFade];
}
else
{
loadingThumbnails = NO;
return;
}
});
}
if(stopThumbnails)
{
dispatch_async( dispatch_get_main_queue(), ^{
loadingThumbnails = NO;
return;
});
}
}
else{
dispatch_async( dispatch_get_main_queue(), ^{
loadingThumbnails = NO;
return;
});
}
}
dispatch_async( dispatch_get_main_queue(), ^{
loadingThumbnails = NO;
return;
});
});
}
}
Any help is of course greatly appreciated :)
Well i dont know if this is the right approach but it works, so i'll mark this as an answer.
To do everything on the background i used a second nsmanagedobjectcontext (MOC) and then merge the changes to the main MOC. the dispatch queue works great although i had to use the NSManagedObjectContextDidSaveNotification in order to merge the changes of the two contexts.
since IOS 5 its possible to use blocks instead that do the merging for you. So i decided to use this instead of the dispatch way (this way i didnt have to use notofications).
Also using blocks i got the same problem (faults) when an object got selected on a background queue while is was deleted on a different queue. So i decided instead of deleting it right away, insert a NSDate 'deleted' property for the OrderItem. then have a timer with a delete check to see if there are objects that have been deleted longer than 10 minutes ago. This way i am sure no thumbnail was still downloading. It works. Though i would still like to know id this is the right approach :)
I have a Core Data model with a Container and Item entities. A Container can have have zero or more Items in it. An Item must belong to at least one Container (but it can be in more than one.)
The relationships look like this:
Container:
Relationship: items, Destination: Item, Inverse: itemContainers
Optional, To-Many Relationship
Delete Rule: Nullify
Item:
Relationship: itemContainers, Destination: Container, Inverse: items
Not-Optional, To-Many Relationship
Delete Rule: Cascade
Problems arise when a Container is deleted. The Item objects in that container are updated, but if the item existed in only one container, the itemContainers property is a set with no objects. Saving the object graph fails because that empty set violates the Item's not-optional setting for itemContainers.
Of course, it's easy enough find the Item objects with empty itemContainers using an NSPredicate like "itemContainers.#count == 0", but it seems like there ought to be a way to configure the model to do this automatically.
So is there an easier/better way?
I tried Tony Arnold's answer above for a similar problem, but found issues when deleting several "Containers" at once (this is on OS X 10.8.2). Containers aren't removed from [item itemContainers] until the managed object context is saved, so count remains above 1 and item never gets deleted.
I came up with the following solution using -[NSManagedObject isDeleted] and category methods on NSManagedObject.
File NSManagedObject+RJSNondeletedObjects.h
#import <CoreData/CoreData.h>
#interface NSManagedObject (RJSNondeletedObjects)
- (NSSet *)RJS_nondeletedObjectsForToManyKeyPath:(NSString *)keyPath;
- (BOOL)RJS_hasOtherNondeletedObjectsForToManyKeyPath:(NSString *)keyPath;
#end
File NSManagedObject+RJSNondeletedObjects.m
#import "NSManagedObject+RJSNondeletedObjects.h"
#implementation NSManagedObject (RJSNondeletedObjects)
- (NSSet *)RJS_nondeletedObjectsForToManyKeyPath:(NSString *)keyPath
{
NSSet * result = nil;
id allObjectsForKeyPath = [self valueForKeyPath:keyPath];
if ( ![allObjectsForKeyPath isKindOfClass:[NSSet class]] ) return result;
result = [(NSSet *)allObjectsForKeyPath objectsPassingTest:^BOOL(id obj, BOOL *stop)
{
BOOL testResult = ![obj isDeleted];
return testResult;
}];
return result;
}
- (BOOL)RJS_hasOtherNondeletedObjectsForToManyKeyPath:(NSString *)keyPath
{
BOOL result = NO;
// self will be in the set of nondeleted objects, assuming it's not deleted. So we need to adjust the test threshold accordingly.
NSUInteger threshold = [self isDeleted] ? 0 : 1;
NSSet * nondeletedObjects = [self RJS_nondeletedObjectsForToManyKeyPath:keyPath];
result = ( [nondeletedObjects count] > threshold );
return result;
}
#end
Container class
...
#import "NSManagedObject+RJSNondeletedObjects.h"
...
- (void)prepareForDeletion
{
NSSet *childItems = [self items];
for (Item *item in childItems) {
if ([item RJS_hasOtherNondeletedObjectsForToManyKeyPath:#"containers"]) {
continue;
}
[managedObjectContext deleteObject:item];
}
}
I know it's not as clean as a configuration option offered by Core Data, but I've deployed a few projects where the Container object cycles through it's child Item entities when it is deleted, checking if they have 0 itemContainers (inside 'Container.m'):
- (void)prepareForDeletion
{
NSSet *childItems = [self items];
for (Item *item in childItems) {
if ([[item itemContainers] count] > 1) {
continue;
}
[managedObjectContext deleteObject:item];
}
}
I don't think you can specify this behavior in your model, butI instead of making that fetch, you could validate the count of itemContainers in your Container's
- (void)removeItemObject:(Item *)value
{...
if(![[value itemContainers]count])
[context deleteObject:value];
...
}
In my app, I make the item's containers relationship optional, and give access to those containerless items via a 'smart container'.
If you don't want that, I suspect you will just have to handle the save failure, and delete the violating objects.
More and more I am changing my approach to core data to a defensive one: assuming validation will fail, and being prepared to handle it. Becomes even more important when you integrate iCloud sync.
I like doing it this way:
- (void)didChangeValueForKey:(NSString *)inKey withSetMutation:(NSKeyValueSetMutationKind)inMutationKind usingObjects:(NSSet *)inObjects
{
[super didChangeValueForKey:inKey withSetMutation:inMutationKind usingObjects:inObjects];
if ([inKey isEqualToString:#"YOURRELATIONSHIP"] && self.YOURRELATIONSHIP.count == 0) {
[self.managedObjectContext deleteObject:self];
}
}