updating NSManagedObject doesn't call NSFetchedResultsControllerDelegate using MagicalRecord - core-data

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.

Related

MagicalRecords fetch data from fetchedResultsController

I have add this fetchedResultsController to my UIViewController. I am using MagicalRecords.
This is my code:
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
_fetchedResultsController = [Artist fetchAllGroupedBy:nil withPredicate:nil sortedBy:#"artist_id" ascending:NO delegate:self];
return _fetchedResultsController;
}
But this code does not invoke.
I have UITableView in my UIViewController. I suppose that this method below should starts the method above but it does not:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id sectionInfo =
[[_fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
So the goal is fetch data using magical records and fetchedResultsController.
I can of course make something like -findAll but as I think fetchedResultsController will update data automatically when it will come instead of -findAll.
You have to declare a property (if you haven't done yet):
#property(strong, nonatomic) NSFetchedResultsController *fetchedResultsController;
in the view controller,
and then access it via the property accessor and not the instance variable, e.g.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
self.fetchedResultsController calls the getter method fetchedResultsController, so that the FRC is created on the first call.
You should also set
_fetchedResultsController.delegate = self;
inside the getter method to enable automatic change tracking, and call
[_fetchedResultsController performFetch:&error];
for the initial fetch (unless MagicalRecord does that for you).

How to use groupBy with MagicalRecord?

I am stuck on how to use groupBy with MagicalRecord.
I have a list of countries with venues
Country -<< Venues
I need to group all the venues by the country and sort the countries by name.
But I am not sure how to do this with MagicalRecord.
I have tried to use a NSFetchedController but sometimes it crashes saying that the array is nil or 0 length.
Other times, it only ever sees 1 category when there are multiple.
Finally, I am not sure how to execute the fetch on an entity.
ie;
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
_objects = [NSMutableArray array];
self.fetchedResultsController = [self fetchedResultsController];
[Venue performFetch:self.fetchedResultsController];
// At this point how do I make the Venue findAllSortedBy work on the performFetch?
_objects = [NSMutableArray arrayWithArray:[Venue findAllSortedBy:#"name" ascending:YES inContext:[NSManagedObjectContext defaultContext]]];
self.title = #"Venues";
}
- (NSFetchedResultsController *)fetchedResultsController {
if (!fetchedResultsController) {
fetchedResultsController = [Venue fetchAllSortedBy:#"name"
ascending:YES
withPredicate:nil
groupBy:#"country"
delegate:self];
}
return fetchedResultsController;
}
-(NSString*)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
NSLog(#"Section = %d", section);
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
NSLog(#"sectionInfo = %#", sectionInfo);
return #"Header";
}
- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
}
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
Venue *v = [_objects objectAtIndex:indexPath.row];
cell.textLabel.text = v.name;
}
I am not sure if I am doing this right.
The above will put everything in 1 country (when there are multiple) and the log will report;
CoreData: error: (NSFetchedResultsController) A section returned nil value for section
name key path 'country'. Objects will be placed in unnamed section
It seems not to see the different countries and I do not think I've done the GroupBy command correctly.
Thus, how do I do a GroupBy command with MagicalRecord?
Many thanks.
self.fetchedVenues = [Venue MR_fetchAllGroupedBy:#"country" withPredicate:nil sortedBy:#"name" ascending:YES];
This error is telling you that of all your Venue objects, there is at least one in your result set that does not have a value for "country". You need to verify that you are indeed filling in this field and saving it properly prior to fetching.
And FYI, in your viewDidLoad method, you don't need all that code. Simply do something like:
- (void) viewDidLoad;
{
[super viewDidLoad];
self.fetchedVenues = [Venue fetchAllSortedBy:#"name" ascending:YES withPredicate:nil groupBy:#"country" delegate:self];
}
fetchAllSortedBy... will perform the fetch for you, and log errors, etc. That is the point of a helper framework like MagicalRecord.

NSManagedObject passed to ViewController Does Reflect All Updates

In my first view controller what I'm doing is setting up a NSManagedObjectContext from a UIMangedDocument in my viewDidLoad
#property(strong, nonatomic) NSManagedObjectContext *managedObjectContext;
- (void)viewDidLoad
{
NSURL *filePath = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
filePath = [filePath URLByAppendingPathComponent:#"Locations"];
UIManagedDocument *document = [[UIManagedDocument alloc] initWithFileURL:filePath];
//Create if it doesn't exist
if (![[NSFileManager defaultManager] fileExistsAtPath:[filePath path]]) {
//Async save
[document saveToURL:filePath forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
if (success) {
self.managedObjectContext = document.managedObjectContext;
}
}];
} else if (document.documentState == UIDocumentStateClosed){
[document openWithCompletionHandler:^(BOOL success){
//Open it, don't need to refetch stuff
if (success) {
self.managedObjectContext = document.managedObjectContext;
}
}];
} else {
self.managedObjectContext = document.managedObjectContext;
}
}
Then I insert a new object via a category method on my NSMangedObject subclass
[Location createLocationWithName:#"Maui" inManagedObjectContext:self.managedObjectContext];
Which just calls this code
Location *location = [NSEntityDescription insertNewObjectForEntityForName:#"Location" inManagedObjectContext:managedObjectContext];
[managedObjectContext save:nil];
Now the problem I'm having is when I segue to a new ViewController that has a public NSManagedObjectContext property and set it to this managedObjectContext in prepareForSegue the NSFetchedResultsController in the destinationViewController doesn't pick up this change right away. After I navigate back a forth a few times it eventually sees the Location Maui I created above. Any ideas why inserting a new Object into the managedObjectContext and then passing it to another view controller doesn't reflect that change?
Any insight is greatly appreciated.
If you creation method contains the name you should at least also set this attribute (otherwise it will be lost). So second line of your creation implmentation:
location.name = name; // name is passed to the method
In order to ensure that the fetched results controller of the second view controller is updated immediately, you could set the cacheName to nil when creating the FRC. If you have lots of records and think you need the cache, you can do this in viewWillAppear:
[self.fetchedResultsController performFetch:&error];

Core Data Delete Rules for many-to-many relationships

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];
}
}

Update/Edit coreData managed object

I'm trying to edit a CoreData object when a user clicks on a cell in a UITableView based on the cell.accessoryType to show if the item has been clicked. Here is the current code.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
NSManagedObject *itemToUpdate = [groceryArray objectAtIndex:indexPath.row];
NSLog(#"updating: %#", itemToUpdate);
if (cell.accessoryType == UITableViewCellAccessoryCheckmark) {
cell.accessoryType = UITableViewCellAccessoryNone;
itemToUpdate.purchased = NO;
}else {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
itemToUpdate.purchased = YES;
}
// Commit the change.
NSError *error;
if (![managedObjectContext save:&error]) {
// Handle the error.
NSLog(#"Saving changes failed: %#", error);
}
}
It seems to be selecting the right object because the NSLog() will show the correct item but when I try to update using the dot notation e.g. "itemToUpdate.purchased = YES;" the compiler throws an error "request for member 'purchased' in something not a structure or union".
I know I'm probably doing this wrong (my first project in xcode) - any advice would be greatly appreciated!
Thanks
Have you tried:
[itemToUpdate setValue:[NSNumber numberWithBool:NO] forKey:#"purchased"]
form?
I always subclass NSManagedObject and the dot notation works for declared properties. But you might try this "older" notation to see if that works.
I suppose you created a custom subclass of 'NSManagedObject' with 'purchased' as one of the properties. Declare 'itemToUpdate' as an object of this subclass, rather than NSManagedObject:
YourCustomSubclassOfNSManagedObject *itemToUpdate = [groceryArray objectAtIndex:indexPath.row];

Resources