xcode Core Data edit value for key issue - objective-c

Got a problem here...
My BOOL gets edited and I get success at the last NSLog, but when I close the ViewController and then go in again (update the table), the BOOL go back to the first value. That will say - something is wrong in my [context save:&error]; function.
Any ideas?
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
for (int i=0; i<[self tableView:tableView numberOfRowsInSection:0]; i++) {
AccountCell *cell = (AccountCell *)[tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
[cell setSelected:(i==indexPath.row) animated:NO];
NSManagedObject *user = [arr objectAtIndex:indexPath.row];
[user setValue:[NSNumber numberWithBool:(i==indexPath.row)] forKey:#"active"];
NSLog(#"Index: %i, Active State: %#", i,[user valueForKey:#"active"]);
NSError *error;
if (![context save:&error]) {
NSLog(#"Saving changes to context failed: %#", error);
} else {
// The changes have been persisted.
NSLog(#"Saved data success");
}
}
}

Some suggestions:
It would make much more sense to put the save statement outside the for loop.
You need to check if
your managed object context is valid (non-nil)
the context of the objects of your mysterious arr array is the same as the context you are saving
the "active" property (including spelling) is correctly configured in your model and the managed object (maybe you want to subclass for more clarity rather than relying on KVC).
there is something in the error variable
I also think there are some other design flaws. For example, you are getting cells and setting their selected state even though they might not even be visible. IMO, you should do this in cellForRowAtIndexPath, based on the state of the underlying managed object.
As for deselecting all other users in the same section you are right that a loop is probably inevitable. But I suppose it would be more efficient to fetch all users in a section at once and then loop through them to set the "active" property as desired.

Related

Changing a managed object property doesn't trigger NSFetchedResultsController to update the table view

I have a fetchedResultsController with a predicate, where "isOpen == YES"
When calling for closeCurrentClockSet, I set that property to NO. Therefore, it should no longer appear on my tableView.
For Some Reason, this is not happening.
Can someone help me figure this out please?
-(void)closeCurrentClockSet
{
NSPredicate * predicate = [NSPredicate predicateWithFormat:#"isOpen == YES"];
NSArray *fetchedObjects =
[self fetchRequestForEntity:#"ClockSet"
withPredicate:predicate
inManagedObjectContext:[myAppDelegate managedObjectContext]];
ClockSet *currentClockSet = (ClockSet *)fetchedObjects.lastObject;
[currentClockSet setIsOpen:[NSNumber numberWithBool:NO]];
}
--
I have a couple of methods more, using the exact same approach,
by calling a custom fetchRequestForEntity:withPredicate:inManagedObjectContext method.
In those methods, when changing a property, tableView get correctly updated!
But this one above (closeCurrentClockSet), doesn't! I can't figure out why.
--
My implementation for my fetchedResultsController, is from Apple's documentation.
Also, another detail. If I send my App, to the background. Close it and re-open, tableView shows updated as it should!
I have tried my best to follow previous questions here on stackOverflow. No luck. I also NSLogged this to the bone.
The object is getting correctly fetched. It is the right one. isOpen Property is being correctly updated to NO. But for some reason, my fetchedResultsController doesn't update tableView.
I did try a couple a "hammer" solutions, like reloadData and calling performFetch. But that didn't work. Or would make sense to used them...
EDIT: scratch that, it DID work, calling reloadData imediatly after performFetch on my resultsController but using reloadData is hammering a solution. Plus, it takes out all animations. I want my controller to auto-update my tableView.
Can someone help me figure this out?
Any help is greatly appreciated!
Thank you,
Nuno
EDIT:
The complete implementation.
fetchedResultsController is pretty standard and straightforward. Everything else is from Apple's documentation
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController) {
return _fetchedResultsController;
}
NSManagedObjectContext * managedObjectContext = [myAppDelegate managedObjectContext];
NSEntityDescription *entity =
[NSEntityDescription entityForName:#"ClockPair"
inManagedObjectContext:managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:entity];
NSString *predicate = [NSString stringWithFormat: #"clockSet.isOpen == YES"];
[fetchRequest setPredicate: [NSPredicate predicateWithFormat:predicate]];
NSSortDescriptor *sortDescriptor1 =
[[NSSortDescriptor alloc] initWithKey:#"clockIn" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor1, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext
sectionNameKeyPath:nil
cacheName:#"Root"];
_fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
--
Boilerplate code from Apple's documentation:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationTop];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationLeft];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationTop];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeSection:(id )sectionInfo
atIndex:(NSUInteger)sectionIndex
forChangeType:(NSFetchedResultsChangeType)type
{
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.tableView endUpdates];
}
1ST UPDATE:
Tracking [managedObjectContext hasChanges] does return YES, as it should. But fetchedResultsController doesn't update the tableView
2ND UPDATE
didChangeObject:atIndexPath: does not get called for this particular case!
I have 2 more methods, with the EXACT same code, they just happen to be a different entity. And they work perfectly. Thank you #Leonardo for pointing this out
3TH UPDATE this method, follows the same rules. But does actually work.
- (void)clockOut
{
NSPredicate * predicate = [NSPredicate predicateWithFormat:#"isOpen == %#", [NSNumber numberWithBool:YES]];
NSArray * fetchedObjects =
[self fetchRequestForEntity:#"ClockPair"
withPredicate:predicate
inManagedObjectContext:[myAppDelegate managedObjectContext]];
ClockPair *aClockPair = (ClockPair *)fetchedObjects.lastObject;
aClockPair.clockOut = [NSDate date];
aClockPair.isOpen = [NSNumber numberWithBool:NO];
}
Anyone has any other ideas for what I might be missing?
Thank you,
Nuno
OK, I will explain your problem, then I will let you judge whether it is a bug in FRC or not. If you think it is a bug, then you really should file a bug report with apple.
Your fetch result controller predicate is like this:
NSString *predicate = [NSString stringWithFormat: #"clockSet.isOpen == YES"];
which is a valid predicate for a boolean value. It is going to follow the relationship of the clockSet entity and grab its isOpen attribute. If it is YES then those objects will be accepted into the array of objects.
I think we are good up to here.
Now, if you change one of clockSet.isOpen attributes to NO, then you expect to see that object disappear from your table view (i.e., it should no longer match the predicate so it should be removed from the array of fetched objects).
So, if you have this...
[currentClockSet setIsOpen:[NSNumber numberWithBool:NO]];
then, whichever top-level object has a relationship to the currentClockSet should "disappear" from your FRC array of fetched results.
However, you do not see it disappear. The reason is that the object monitored by the FRC did not change. Yes, the predicate key path changed, but the FRC holds entities of ClockPair and a ClockSet entity actually changed.
You can watch the notifications fly around to see what's going on behind the scenes.
Anyway, the FRC will use a key path when you do a fetch, but it will not monitor changes to objects that are not in its actual set of fetched objects.
The easiest work-around is to "set" an attribute for the object that holds this key path object.
For example, I noticed that the ClockPair also has an isOpen attribute. If you have an inverse relationship, then you could do this...
currentClockSet.isOpen = NO;
currentClockSet.clockPair.isOpen = currentClockSet.clockPair.isOpen;
Notice that you did not actually change the value at all. However, the setter was called, which triggered KVO, and thus the private DidChange notification, which then told the FRC that the object changed. Thus, it re-evaluates the check to see if the object should be included, finds the keypath value changed, and does what you expect.
So, if you use a key path in your FRC predicate, if you change that value, you need to worm your way back to all the objects in the FRC array and "dirty them up" so that those objects are in the notification that is passed around about object changes. It's ugly, but probably better than saving or changing your fetch request and refetching.
I know you don't believe me, so go ahead and try it. Note, for it to work, you have to know which item(s) in the FRC array of objects would be affected by the change, and "poke" them to get the FRC to notice the change.
The other option, as I mentioned earlier, is to save the context, and refetch the values. If you don't want to save the context, you can make the fetch include updates in the current context, without refreshing from the store.
I have found that faking a change to an object that the FRC is watching is the best way to accomplish a re-evalution of predicates that are key paths to other entities.
OK, so, whether this is a bug or not is up for some debate. Personally, I think if the FRC is going to monitor a keypath, it should do it all the way, and not partially like we see here.
I hope that make sense, and I encourage you to file a bug report.
You ran into a similar problem.
I know this question is pretty old but I hope this helps someone else:
The easiest way was to introduce a new property named lastUpdated: NSDate in the parent object.
I had a Conversation which contains several Messages. Whenever the isRead flag of the message was updated, I needed an update in the ConversationOverviewViewController that only displays Conversations. Furthermore, the NSFetchedResultsController in the ConversationOverviewVC only fetches Conversations and doesn't know anything about a Message.
Whenever a message was updated, I called message.parentConversation.lastUpdated = NSDate(). It's an easy and useful way to trigger the update manually.
Hope this helps.
After [currentClockSet setIsOpen:[NSNumber numberWithBool:NO]]; can you save the managed object context:
NSError *saveError = nil;
if( ![[myAppDelegate managedObjectContext] save:&saveError] ) {
// handle error saving context
}
I suspect your UITableView will update properly after saving the context. This is likely why sending your app to the background works. I suspect that your Core Data stack is set up in the application's delegate in such a way that it performs a save on the main NSManagedObjectContext when it goes into the background.

NSRangeException when using Core Data Asynchronous UISearchDisplayController

I'm asynchronously fetching data, and I've used this as a guide: http://deeperdesign.wordpress.com/2011/05/30/cancellable-asynchronous-searching-with-uisearchdisplaycontroller/
In
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString{
//setup request / predicate etc...
[self.searchQueue addOperationWithBlock:^{
NSError *error;
self.matchingObjects = [self.managedObjectContext executeFetchRequest:request error:&error];
[request release];
[[NSOperationQueue mainQueue] addOperationWithBlock:^
{
[self.searchDisplayController.searchResultsTableView reloadData];
}];
}];
// Return YES to cause the search result table view to be reloaded.
return NO;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
// Return the number of rows in the section.
return [self.matchingObjects count];
}
Every now and then I'll get something to the effect of:
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSArray objectAtIndex:]: index 0 beyond bounds for empty array'
This is thrown at the matchingObjects ivar when accessing it to construct the table cell in:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
The crash doesn't occur all the time, just seems to happen on random occasions. I'm guessing that somewhere the count on the matchingObjects array is returning a certain value, which changes and is not being updated.
I'm not entirely sure of how to deal with this - been looking over this for hours, is there something I'm missing?
I figured out what it was - took me a while, but I looked again at the example that I just linked. I was updating the self.matchingObjects iVar in the background thread, which on some occasions caused a mismatch between the range of the array available in the main thread and the background thread. So for example, the variable may have been updated in the background thread, and the main thread may still be accessing a part of the range that no longer exists in the variable since it was updated.
Fixed it by amending my code as follows:
[self.searchQueue addOperationWithBlock:^
{
NSError *error;
NSArray *results = [self.managedObjectContext executeFetchRequest:request error:&error];
[request release];
[[NSOperationQueue mainQueue] addOperationWithBlock:^
{
self.matchingObjects = results;
[self.searchDisplayController.searchResultsTableView reloadData];
}];
}];
Now the results of the search are loaded into a temporary holding array named "results", and the matchingObjects iVar is first updated in the main thread and then the tableView is reloaded. This way, the tableView always is referring to an array that is never changed whilst it is being accessed, since the tableView relies on matchingObjects to get the number of rows and data.

NSTable loses focus after present:error

I have an NSTableView that lists tags that are stored using Core Data. The default value for a tag is 'untitled' and I need each tag to be unique, so I have a validation routine that traps empty and non-unique values and that works fine. I don't want the user to be able to store the 'untitled' value for a tag, so I am observing the NSControlTextDidEndEditingNotification, which calls the following code:
- (void)textEndedEditing:(NSNotification *)note {
NSString *enteredName = [[[note userInfo] valueForKey:#"NSFieldEditor"] string];
if ([enteredName isEqualToString:defaultTagName]) {
NSString *dString = [NSString stringWithFormat:#"Rejected - Name cannot be default value of '%#'", defaultTagName];
NSString *errDescription = NSLocalizedStringFromTable( dString, #"Tag", #"validation: default name error");
NSString *errRecoverySuggestion = NSLocalizedStringFromTable(#"Make sure you enter a unique value for the new tag.", #"Tag", #"validation: default name error suggestion");
int errCode = TAG_NAME_DEFAULT_VALUE_ERROR_CODE;
NSArray *objArray = [NSArray arrayWithObjects:errDescription, errRecoverySuggestion, nil];
NSArray *keyArray = [NSArray arrayWithObjects:NSLocalizedDescriptionKey, NSLocalizedRecoverySuggestionErrorKey, nil];
NSDictionary *eDict = [NSDictionary dictionaryWithObjects:objArray forKeys:keyArray];
NSError *error = [[NSError alloc] initWithDomain:TAG_ERROR_DOMAIN code:errCode userInfo:eDict];
NSBeep();
[preferencesWindowsController presentError:error];
unsigned long index = [self rowWithDefaultTag];
[self selectRowIndexes:[NSIndexSet indexSetWithIndex:index] byExtendingSelection:NO];
// [self editColumn:0 row:index withEvent:nil select:YES];
}
}
- (unsigned long)rowWithDefaultTag {
__block unsigned long returnInt;
[managedTags enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if ([[obj valueForKey:#"name"] isEqualToString:defaultTagName]) {
returnInt = idx;
*stop = YES;
}
}];
return returnInt;
}
With the 'editColumn' line commented out, the code works, so if the user accepts the default tag name without editing it, the error is built, displayed and the process finishes by leaving the appropriate row in the table highlighted.
However, I would like to take it that step further and place the user in edit mode. When I uncomment the 'editColumn' line, the behaviour is not at all what I expected - the tableView loses its blue focus box and the row that respresents the new tag is blank. If I click on the tableView, the row becomes visible. I've spent a lot of time on this and have got nowhere, so some help with this would be very much appreciated.
(Note: I tried using textDidEndEditing, which also didn't behave as I expected, but that is a separate issue!)
Answering my own question. Doh!
I already had a method which I used to put the user in edit mode when they clicked the button to add a new tag:
- (void)objectAdded:(NSNotification *)note {
if ([[note object] isEqual:self]) {
[self editColumn:0 row:[self rowWithDefaultTag] withEvent:nil select:YES];
}
}
Creating a notification to call this solves the problem and places the user in edit mode correctly. The important thing is not to try to do this on the existing runloop; so sending the notification as follows postpones delivery until a later runloop:
// OBJECTADDED is a previously defined constant.
NSNotification * note = [NSNotification notificationWithName:OBJECTADDED object:self];
[[NSNotificationQueue defaultQueue] enqueueNotification: note postingStyle: NSPostWhenIdle];
Problem solved. I wasted a lot of time trying to solve this - a classic example of getting too involved in the code and not looking at what I'm trying to do.
I've forgotten where I first saw this posted - whoever you are, thank you!

Trouble with Asynchronous Downloading UITableView

I am trying to asynchronously download images for a UITableViewCell, but it is currently setting the same image to each cell.
Please can you tell me the problem with my code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
}
SearchObject *so = (SearchObject *)[_tableData objectAtIndex:indexPath.row];
cell.textLabel.text = [[[[so tweet] stringByReplacingOccurrencesOfString:#""" withString:#"\""] stringByReplacingOccurrencesOfString:#"<" withString:#"<"] stringByReplacingOccurrencesOfString:#">" withString:#">"];
cell.detailTextLabel.text = [so fromUser];
if (cell.imageView.image == nil) {
NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:[so userProfileImageURL]]];
NSURLConnection *conn = [NSURLConnection connectionWithRequest:req delegate:self];
[conn start];
}
if ([_cellImages count] > indexPath.row) {
cell.imageView.image = [UIImage imageWithData:[_cellImages objectAtIndex:indexPath.row]];
}
return cell;
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[_cellData appendData:data];
[_cellImages addObject:_cellData];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[self.tableView reloadData];
}
You are appending the data from every image downloaded to the same data object. So in the best case the data object ends up with the data for image #1 immediately followed by the data for image #2 and so on; the image decoder is apparently taking the first image in the chunk of data and ignoring the garbage after. You also seem to be unaware that NSURLConnections' connection:didReceiveData: will not necessarily be called in the order that the connections were started, that connection:didReceiveData: can be called zero or multiple times per connection (and probably will if your images are more than a few kibibytes), and that tableView:cellForRowAtIndexPath: is not guaranteed to be called for every cell in the table in order. All of which are going to totally screw up your _cellImages array.
To do this right, you need to have a separate NSMutableData instance for each connection, and you need to add it to your _cellImages array just once, and at the correct index for the row rather than at the arbitrary next available index. And then in connection:didReceiveData: you need to figure out the correct NSMutableData instance to append to; this could be done by using the connection object (wrapped in an NSValue using valueWithNonretainedObject:) as the key in an NSMutableDictionary, or using objc_setAssociatedObject to attach the data object to the connection object, or by making yourself a class that handles all the management of the NSURLConnection for you and hands you the data object when complete.
I don't know if this is causing the problem or not, but in your connection:didReceiveData: method you're just appending the image data to the array; you should be storing the image data in such a way that you can link it to the cell it's supposed to be shown in. One way to do this would be use an NSMutableArray populated with a bunch of [NSNull]s, then replace the null value at the appropriate index when the connection has finished loading.
Also, you're appending the _cellData to the _cellImages array when the connection hasn't finished loading, you should only be doing this in the connection:didFinishLoading method.

NSManagedObjectContext: autoupdate or not?

I need to understand something about NSManagedObjectContext update.
I have a UISplitView with a UITableViewController on the RootView and a UIViewController on the Detail View. When I tap in a row with data, I load some data into labels and a UITextView where I can update that field:
- (void)textViewDidEndEditing:(UITextView *)textView {
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
[[listOfAdventures objectAtIndex:indexPath.row] setAdventureDescription:textView.text];
}
Ok. This works correctly, the description is updated.
Also, someone might wants to delete a row:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"playerPlaysAdventure.adventureName==%#",[[listOfAdventures objectAtIndex:indexPath.row] adventureName]];
NSArray *results = [[AdventureFetcher sharedInstance] fetchManagedObjectsForEntity:#"Player" withPredicate:predicate withDescriptor:#"playerName"];
[moc deleteObject:[listOfAdventures objectAtIndex:indexPath.row]];
for ( Player *player in results ) {
[moc deleteObject:player];
}
[listOfAdventures removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:YES];
[self clearDetailViewContent];
NSError *error = nil;
if ( ![moc save:&error] ) {
NSLog( #"Errore nella cancellazione del contesto!" );
abort();
}
}
else if (editingStyle == UITableViewCellEditingStyleInsert) {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
So here's my problem: if I comment the rows about the saving of my MOC, the adventure is only momentarily deleted. If you quit the app and the reopen it, the object is still there. This doesn't happen with the update of a field. I'd like to know why and if I should save moc also in textViewDidFinishEditing method.
Thank you in advance.
It's the difference between changing an attribute of an object and adding or removing an entire object in the object graph.
In the first block, you change an attribute of an existing object which saves automatically unless you run an undo. This is because the object already exist in the object graph and no other objects have to be altered to make the change.
In the second block, you are removing an entire object and potentially altering the object graph itself by changing the relationships between objects. That change will not be committed until an implicit save because potentially it can trigger a cascade of changes throughout a large number of objects.