I have an NSArrayController bound to a Core Data entity — Channel. I have a method that updates the entity after a user changes the title of the Channel:
NSManagedObjectContext *localContext = [NSManagedObjectContext MR_contextForCurrentThread];
NSString * savedTitle = [fieldEditor string];
self.mainWindowController.selectedChannel.channel_name = savedTitle;
[localContext MR_saveToPersistentStoreAndWait];
(I'm using MagicalRecord, in case that's not clear)
By all accounts, this save works — the value is reflected when the change is made, and if I immediately check the persistent store using Core Data Editor I can see the change has been saved.
The problem occurs when I change the view — the change is made in a master view, and the user can switch to a detail view. This line of code, which swaps out the master view, marks the spot where trouble occurs:
[[self.chatsViewController.view animator] removeFromSuperview];
After this line executes, the newly-saved value is gone. Requests to the entity attribute returns an empty string.
But here's the weird part. If I restart the application, the new value is still there! It comes back, and this time it's here to stay. Also, looking at the persistent store, I never see the value disappear at any point. This suggests that my array controller is getting the value taken away somehow.
I've tried refreshing the array controller after the save to "lock it in", as it were. But this appears to have no effect.
Can anyone suggest what steps I might take to track this down? I can't fathom why removing a view would cause this to happen.
Update
As I noted in my comment below, I tried putting a log statement on the name attribute's setter in my entity subclass, and it yielded a log entry when I changed the name, but never after.
I've now done another experiment: looking at the Core Data record both before and after the view switch.
NSArray * channels = [Channel MR_findAll];
NSLog(#"Channel: %#", [[channels objectAtIndex:0] channel_name]);
[[self.chatsViewController.view animator] removeFromSuperview];
NSArray * channels2 = [Channel MR_findAll];
NSLog(#"Channel2: %#", [[channels2 objectAtIndex:0] channel_name]);
The result:
Channel: adfasd
Channel2:
I have to be missing something here.
Related
I have a NSTableView with an NSTableColumn with an NSButtonCell (a checkbox) inside it, which generates a new instance of NSButtonCell each time a row is added, which I configured in IB. However, I'm curious why in the following chunk of code the second NSLog returns 0.
NSLog(#"%ld", (long)[[self.tableView preparedCellAtColumn:0 row:0]state]);
[[self.tableView preparedCellAtColumn:0 row:0]setState:1];
NSLog(#"%ld", (long)[[self.tableView preparedCellAtColumn:0 row:0]state]);
The fact that it returns 0 means that I am sending a message to an instance of NSButtonCell, right? So why doesn't setState: change the return value of the second NSLog?
If it was receiving nil it would also print 0, I would suggest trying this
NSLog(#"cell:%#", [self.tableView preparedCellAtColumn:0 row:0])
to ensure you are actually getting a valid cell object from the table view.
Where are you calling that code from? After the table is already being displayed? During initialisation?
If the former then there should be a cell available, if the latter then it may not have been created or reallocated from the pool yet.
Try the NSLog command above to ensure you are actually getting a cell back and not nil from the table view.
I'm facing a strange issue where an NSFRC fetchedObjects array returning not all the objects it should. To give you some context, my application has several list view controllers, each of them having an NSFRC. I'm updating the list view within the delegate method controllerDidChangeContent. The problem I'm facing is the following: after storing an object in a background MOC and saving it, the controllerDidChangeContent is invoked but the object I just saved in the background thread doesn't show up in the NSFRC. Here is a piece of code that I'm using to check this:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
NSManagedObjectContext *context = controller.managedObjectContext;
NSError *error = nil;
NSArray *array = [context executeFetchRequest:controller.fetchRequest error:&error];
if (nil != array) {
NSUInteger count = MIN(controller.fetchedObjects.count, array.count);
for (NSUInteger index=0; index<count; index++) {
NSManagedObject *a = array[index];
NSManagedObject *b = controller.fetchedObjects[index];
// Here you will see that sometimes the objects don't match
NSLog(#"%d: %# <--> %#", index, [[a body] text], [[b body] text]);
}
}
}
I'm expecting the NSFRC fetchedObjects array to be identical to the array returned by a manual executeFetchRequest (I'm using the NSFRC fetchRequest to manually fetch the data). However, this is not the case. The manual executeFetchRequest returns more object than the NSFRC fetchedObjects. Does anyone know what's going on? I've turned the caching on the NSFRC off but the same behavior is reported.
Thanks!
=== Update ====
Some update on that issue. I think there is a bug in Core Data because I was able to see some inconsistent results from the NSFRC and moreover was able to fix the problem by a workaround involving "touching" the object in question. Here is a scenario that explains what is happening:
Imagine the following Core Data model where:
- There are Cat objects and Master objects.
- A Cat can have one or more Master.
- A Master can have one or more Cat.
- A first NSFRC (let's call it NSFRC_A) is created to fetch all the cats with master named "Master_A". The predicate is { ANY master.name == "Master_A" }.
- A second NSFRC (let's call it NSFRC_B) is created to fetch all the cats with master named "Master_B". The predicate is { ANY master.name == "Master_B" }.
- There is a main managed object context that is used in the UI thread only
- There is a background managed object context created for each background thread, using the same persistent store as the main managed object context.
A cat named "Cat_A" is created in the background and assigned to master "Master_A". After the background context is saved, the main context is updated appropriately. At this point, the NSFRC_A notifies its delegate that a change has occurred and correctly reports "Cat_A".
Later on, in a background thread, the same cat "Cat_A" is assigned master "Master_B". After the background context is saved, the main context is updated appropriately. At this point, the NSFRC_A notifies its delegate of that change and correctly reports "Cat_A". NSFRC_B also notifies its delegate of that change but doesn't report "Cat_A" (it is missing from its fetchedObjects). However, if I manually perform a fetch using the same fetchRequest as NSFRC_B, I can see "Cat_A" being returned. The weird thing is that the "Cat_A" instance being returned is marked as a fault which explains why NSFRC_B doesn't return the "Cat_A" because it doesn't see it in memory.
This is a bug because I can fix that behavior by simply logging the "Cat_A" relationship to master when the changes from the background thread are merged into the main context: the logging basically touches the object and forces it to be realized into memory.
The problem appears to be a limitation of the NSFRC. According to this thread on the Apple Forum (https://devforums.apple.com/message/765374): "The limitation being that a fetched results controller for entity A won't always catch an update to entity B that would cause the predicate to change.". To solve the issue, I had to dirty the object I'm looking for before it is being merged into the main thread: then the NSFRC detects that change.
I want a thorough list regarding comparison between the two. Things I have known:
executeFetchRequest:
Message sent to MOC
Return an array of managed objects
Goal: fetch objects from persistent store to MOC
With table view: has nothing to do with table view
Frequency: often used in a loop, so could be called many many times
performFetch:
Message sent to FRC
After calling it, use fetchedObjects to return an array of managed objects
With table view: FRC is specifically for keeping managed objects and table view rows in sync, and use performFetch to initialize that process.
Frequency: often only once. Unless fetch request of FRC changes, no need to call performFetch a second time
Please correct me if I am wrong and append the list. Thank you.
About executeFetchRequest:
Message sent to MOC
Yes
Return an array of managed objects
Yes, but you can also change the type of results you want to retrieve. In NSFetchRequest you can set a different result type with:
- (void)setResultType:(NSFetchRequestResultType)type
where NSFetchRequestResultType can be of different types. Taken from Apple doc:
enum {
NSManagedObjectResultType = 0x00,
NSManagedObjectIDResultType = 0x01,
NSDictionaryResultType = 0x02
NSCountResultType = 0x04
};
typedef NSUInteger NSFetchRequestResultType;
Goal: fetch objects from persistent store to MOC
Yes, creating a NSFetchRequest and performing a request, it the same as creating a SELECT statement in SQL. If you also use a NSPredicate it's the same as using SELECT-WHERE statement.
With table view: has nothing to do with table view
Yes, but with retrieved data you can populate a table
Frequency: often used in a loop, so could be called many many times
It depends, on what you want to achieve. It could be within a loop or not. Executing the request within a loop could have impact on performance but I would not be worried on that. Under the hood Core Data maintains a sort of cache mechanism. Every time you perform a request, if data are not in the cache, Core Data executes a round trip on your store (e.g. sql file) and populate the cache with the objects it has retrieved. If you perform the same query, the round trip will not performed again due to the cache mechanism. Anyway, you could avoid to execute a request within the run loop, simply moving that request outside the loop.
About performFetch:
Message sent to FRC
Yes
After calling it, use fetchedObjects to return an array of managed
objects
Yes, but you can also retrieve an object with [_fetchedResultsController objectAtIndexPath:indexPath]; if you are populating a specific cell within a table.
Here I really suggest to read a nice tutorial on NSFetchedResultsController
With table view: FRC is specifically for keeping managed objects and
table view rows in sync, and use performFetch to initialize that
process.
Yes, a NSFetchedResultsController works in combination with a NSManagedObjectContext for you. Furthermore, it enables lazy loading of data. Suppose you have 1000 elements you retrieve and you want to display them in a UITableView. Setting a request for a NSFetchRequest like:
[fetchRequest setFetchBatchSize:20];
and using it with an instance of a NSFetchedResultsController, it allows to load 20 elements at first. Then when you scroll, other 20 elements are loaded, and so on. Without a NSFetchedResultsController you must implement this behavior manually. Refer to the tutorial I provided for further info.
Frequency: often only once. Unless fetch request of FRC changes, no
need to call performFetch a second time
It depends on what you want to achieve. Most of the time you could call it once.
Hope that helps.
Edit
You have to call performFetch explicitly. I like to create a property for NSFetchedResultsController in my header file (.h) like
#property (nonatomic, strong, readonly) NSFetchedResultsController* fetchedResultsController;
and synthesize it in your implementation file (.m) like
#synthesize fetchedResultsController = _fetchedResultsController;
Then always within the .m file override the getter to create an new instance of it:
- (NSFetchedResultsController*)fetchedResultsController
{
// it already exists, so return it
if(_fetchedResultsController) return _fetchedResultsController;
// else create it and return
_fetchedResultsController = // alloc-init here with complete setup
return _fetchedResultsController;
}
Once done, within your class (for example in viewDidLoad method) use it like
NSError *error = nil;
if (![[self fetchedResultsController] performFetch:&error]) {
// Handle the error appropriately.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
You are comparing the wrong elements. NSFetchedResultsController uses the NSManagedObjectContext to perform the fetch, and under proper configuration, monitors the changes in the managed object context to verify the status of the fetch properties it is monitoring, but the actual fetches are done by the context. On both cases, NSManagedObjectContext does the fetch. The difference being that, using the NSManagedObjectContext directly, you get an NSArray type of object (the actual runtime class is different than an array you get using [NSArray array]), while NSFetchedResultsController has a different purpose (have a collection of results and monitor changes to the records and entity on its fetch request). In other words, NSFetchedResultsController works using the context, but it works different than just a simple collection of objects.
One observation: you shouldn't be using executeFetchRequest inside a loop, especially calling it "many many times". Each fetch has its performance cost. You can call executeFetchRequest once, and do a loop to check the result.
I have an app which uses a table view to display a list of items form my core data. I am using a remote api and I am updating my content after pulling down the table view - this triggers a call to the API.
Data is retrieved, parsed and inserted/updated into my Core Data.
I am sometimes getting an error after saving my Core Data context... Note that I'm not using multiple threads for this and like I said it doesn't seem to always happen.
I am literally going quite mad. It seems this guy has a similar issue but I'm still unable to fix mine with his solution:
CoreData error driving me crazy... CoreData: Serious application error. An exception caught from delegate of NSFetchedResultsController
Here is the full error:
2012-07-31 14:14:47.332 MyApp[2893:11303]
*** Assertion failure in -[_UITableViewUpdateSupport _setupAnimationsForNewlyInsertedCells],
/SourceCache/UIKit_Sim/UIKit-1914.84/UITableViewSupport.m:1133
2012-07-31 14:14:47.332 MyApp[2893:11303] CoreData: error: Serious application error.
An exception was caught from the delegate of NSFetchedResultsController during a call to -
controllerDidChangeContent:.
Attempt to create two animations for cell with userInfo (null)
UPDATE:
I have a predicate on my fetch request. In order to seem to be deleting objects previously downloaded from API and which are missing from the new JSON result. I am setting a hideFromUser flag, this is saved in my Core Data.
If this flag is YES then it doesn't appear in the table view. But if it's ok then it does. I am also updating info on that managed object should anything be changed. Is it possible that I have an object which was previously set to hide... and is now set to show, and it also had some new data, could this possible trigger a "cell should update" and a "cell should insert" ?
More I think about it less it seems to be relevant.
Here is how I am updating my data:
1) I set all relevant objects of the corresponding type to "hide form user" (NSPredicate ensures they don't show in Table View).
2) I get an NSArray from the JSON data.
3) Looping each item, my createABookOfClass:withJSON: method queries the core data for a book (using an ID from the json dictionary), if it doesn't find it, it creates a new one. Note: at this point the "hide from user flag" is reverted.
4) After all is done, I save.
[[DPLocalStore getInstance] hideFlagItemsOfType:NSStringFromClass([MyFavouriteBook class])];
NSArray * itemsJSON = [data mutableObjectFromJSONData];
[itemsJSON enumerateObjectsUsingBlock:^(NSDictionary *obj, NSUInteger idx, BOOL *stop) {
[[DPLocalStore getInstance] createABookOfClass:[MyFavouriteBook class]
withJSON:obj];
}];
NSError *error = nil;
BOOL didsave = [[DPLocalStore getInstance] save:&error];
Maybe what is happening is a cell containing Object A has been updated, it's update: the hide flag has changed. thus I am getting into a situation where the NSFetchedResultsController's delegate wants to update that cell, and delete it also... since the predicate now doesn't correspond to this object... That sound very likely...
I think I may have found the error, since I changed it I haven't experienced the problem so I am assuming that it's ok.
In my implementation of controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: I had this in the switch statement for NSFetchedResultsChangeMove:
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
// Reloading the section inserts a new row and ensures that titles are updated appropriately.
[tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section]
withRowAnimation:UITableViewRowAnimationFade];
break;
As it was used in an older project (which I was not part of), I assumed the entire controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: implementation was okay, since this really is more of a template rather then something you will do anything customised.
In consequence I didn't pay much attention to it until I was reading up about it again on the Apple developer site. And I noticed, in case of a NSFetchedResultsChangeMove you must delete a cell, and insert the cell at the new path. Yet I had implemented without really realising - delete the cell and reload the section !
In effect I should and now have the following:
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
Like I said I've not had a problem since. Would love to know from anyone what inner workings would be responsible for this - the exception having been "Attempt to create two animations for cell".
Thanks for any interest.
During the creation of a Core Data entity (Event), I am creating a relationship to another entity (Team). This relationship is many-to-one from Team to Events (one team, many events) and has an inverse relationship from Event to Team.
Team<----->>Event.
The delete rule for both relationships is set to 'Nullify'.
The below block of code works successfully on first population when a new Team is created during the creation of each Event. However, if I then remove an Event and attempt to re-add it, the existing Team is retrieved but the code fails when attempting to add the Team object to the Event in the final line of the example. The error is as follows: -[__NSCFDictionary managedObjectContext]: unrecognized selector sent to instance 0x699ed60
What is the correct way to create a relationship between the Event object to the Team object that already exists?
Team *currentTeam = self.team;
Team *newTeam = (Team *)[self loadTeamForNid:[NSNumber numberWithInteger: [teamNid integerValue]]];
// If the nid of the referenced team has changed,
if (![[[currentTeam nid] stringValue] isEqualToString:teamNid]) {
currentTeam = nil;
currentTeam = newTeam;
}
// If an event has not been set by this point, it does not exist in the CD store, and we need to create it.
if (currentTeam == nil) {
currentTeam = (Team *)[NSEntityDescription insertNewObjectForEntityForName:#"Team" inManagedObjectContext:[delegate managedObjectContext]];
[currentTeam populateTeamWithNode:[node nodeGet:teamNid]];
}
// TODO: This breaks on reload of an object
// self.team = currentTeam;
[self setValue:currentTeam forKey:#"team"];
Conceptually, you aren't mistaken: you set the event's "team" property to an instance of NSManagedObject that represents the appropriate team.
This message:
-[__NSCFDictionary managedObjectContext]: unrecognized selector sent to instance 0x699ed60
Means that some line of code is handling an instance of NSDictionary where it expects (I assume) an instance of NSManagedObject. When it tries to query the object's managedObjectContext, an exception is thrown, because an NSDictionary doesn't implement a method for that selector.
The first thing to do is put a breakpoint on that last line and see if currentTeam is actually an NSDictionary in disguise. (This seems unlikely, given the code above an exception would have been hit earlier.) If not, you'll have to hunt around for related properties that might be involved in this code path.
Note that Core Data supports a fetch request style where it returns NSDictionary instances instead of NSManagedObjects; if you are using this anywhere in your code, you might be accidentally passing the result along to another method that doesn't expect it.