Strange problem, and strangely only since yesterday. I have a fetched results controller in which you can reorder the rows without any problem. After a fresh start of the app, everything works fine and fast. However if you change from this tab bar to another tab bar and edit some random textfield (which isn't even linked to core data and doesn't trigger any save), the reordering is very slow. And I was able to narrow it down only to the save context. But now I have no clue where to look further. Any advice? Here's my reorder function:
- (void)tableView:(UITableView *)tableView
moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath
toIndexPath:(NSIndexPath *)destinationIndexPath;
{
if (self.activeField && [self.activeField isFirstResponder]){
[self.activeField resignFirstResponder];
}
self.suspendAutomaticTrackingOfChangesInManagedObjectContext = YES;
//Makes only a mutable copy of the array, but NOT the objects (references) within
NSMutableArray *fetchedResults = [[self.fetchedResultsController fetchedObjects] mutableCopy];
// Grab the item we're moving
NSManagedObject *resultToMove = [self.fetchedResultsController objectAtIndexPath:sourceIndexPath];
// Remove the object we're moving from the array.
[fetchedResults removeObject:resultToMove];
// Now re-insert it at the destination.
[fetchedResults insertObject:resultToMove atIndex:[destinationIndexPath row]];
// All of the objects are now in their correct order. Update each
// object's displayOrder field by iterating through the array.
int i = 1;
for (MainCategory *fetchedResult in fetchedResults)
{
fetchedResult.position = [NSNumber numberWithInt:i++];
}
// Save
TICK;
[((AppDelegate *)[[UIApplication sharedApplication] delegate]) saveContext];
TOCK;
self.suspendAutomaticTrackingOfChangesInManagedObjectContext = NO;
}
Yesterday I've only inserted these two methods (which cannot be the problem, because after commenting them so they don't trigger, the same problem remained):
-(void) registerForKeyboardNotificationsWithTabBarHeight:(CGFloat)tabbarHeight andHeaderHeight:(CGFloat)headerHeight andFooterHeight:(CGFloat)footerHeight andBottomSpacingToTextFields:(CGFloat)spacingToTextFields
{
self.tabbarHeight = tabbarHeight;
self.footerHeight = footerHeight;
self.headerHeight = headerHeight;
self.spacingToTextFields = spacingToTextFields;
// Register notification when the keyboard will be show
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:nil];
// Register notification when the keyboard will be hide
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
FetchedResultsController:
- (void)setupFetchedResultsController
{
self.managedObjectContext = ((AppDelegate *)[[UIApplication sharedApplication] delegate]).managedObjectContext;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"MainCategory"];
request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"position" ascending:YES]];
self.fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:request
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil cacheName:#"MainCategoryCache"];
}
And the view will appear is also very harmless..
- (void)viewWillAppear:(BOOL)animated
{
//Initialize database
[super viewWillAppear:animated];
[self displayBudget];
/*
This is only necessary since the sum of all categories is calculated and the FetchedResultsController
doesn't get this since it's only monitoring a MainCategory and the changes occured in the Subcategories */
[self.tableView reloadData];
}
I'm thankful for any clues what could be the problem :-(
I would not recommend multi-threading in this situation. The issue is that your saves are being done at the wrong time. Moving them to a background queue is not going to solve the issue. You need to change your save behavior.
Writing to disk is expensive. There is no shortcut around it. Even if you move your saves into another context (there is an Apple example on this) they are still doing to take time and potentially block your UI (you cannot do a new fetch during a save). You need to change your save behavior.
You do not need to save every time a user moves a row. That is wasteful. You want to queue up your saves so that you are doing more each save. In addition, you should be saving when the user expects there to be a delay. When you push or pop a view might be a good time for a save. When your app goes into the background is a great time for a save.
Saving when the user wants smooth UI response is bad and causes a bad user experience. Look for opportunities where the user is expecting something to take time. Posting something to a server? Save at the same time.
Your NSFetchedResultsController, etc. will read unsaved data from your main NSManagedObjectContext just as it will saved data. You are not gaining anything on the UI by saving frequently.
Update 1
Step one in performance problems is to run your application in Instruments.
Measure the speed and then see what is costing you time. Time Profiler is your friend.
Yes it will slow drawing your UI because you should't use saving and creating objects in main Queue;
read this https://developer.apple.com/library/ios/documentation/cocoa/conceptual/CoreData/Articles/cdConcurrency.html
you should have another background thread and you should do inserting deleting stuff there and then you have to pass NSObjectID_s for newManagedObject_s and then get it in main thread:
NSManagedObjectContext * mainThreadContext;
NSManagedObject * object = [mainThreadContext objectWithId:objectID];
Related
First thing I tried was to use a FetchedResultsController to solve my problem. This was an atypical use of FRC in that I was not updating a TableView, I was using it just to determine if entities were changing so I knew when to save to the server:
self.fetchedResultsController = [Document MR_fetchAllSortedBy:#"timestamp"
ascending:NO
withPredicate:[NSPredicate predicateWithFormat:#"report.remoteId != nil && dirty == YES"]
groupBy:nil
delegate:self
inContext:_managedObjectContext];
The issue here is that FRC does not receive updates when a relationship entity changes. IE if a report.remoteId goes from nil to non-nil I will not see the update as the FRC listens for changes on only the Document entity. Limitation outlined here: http://www.mlsite.net/blog/?p=825 and here Changing a managed object property doesn't trigger NSFetchedResultsController to update the table view
Not sure I really want to implement the workaround as I fell like I am using the FRC for things it was not meant to do. I don't think apple is going to fix this limitation, so I don't really want to smash a round peg into a square hole to solve my problem.
A couple of options
Fire a notification in code after I have saved entities, then listen for that elsewhere. Only thing I don't like about this is that it up to the programmer to do this and keep it up to date, ie if someone comes along and saves an entity in another place they must remember to fire the notification.
OR
Listen for saves to the default MOC. This is what I would really like to do. Something like this:
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(handleDataModelChange:)
name:NSManagedObjectContextObjectsDidChangeNotification
object:[NSManagedObjectContext MR_defaultContext]];
- (void)handleDataModelChange:(NSNotification *)note {
NSSet *updatedObjects = [[note userInfo] objectForKey:NSUpdatedObjectsKey];
NSSet *deletedObjects = [[note userInfo] objectForKey:NSDeletedObjectsKey];
NSSet *insertedObjects = [[note userInfo] objectForKey:NSInsertedObjectsKey];
// Do something in response to this
}
Here is my dilemma with this. This will listen to all changes on the default MOC. I really only care about changes to a couple of entities. So yeah I can filter out the entities I care about in each call. BUT, I have a case in which I save a ton of other entities in which I do not care about. This means that the NSManagedObjectContextObjectsDidChangeNotification will fire a ton and most of the time I will not care. I don't want to slow down my app by constantly receiving this notification and taking time to filter out all entities that I do not care about.
Is there a way to listen for specific entity saves? Either in CoreData, or MagicalRecord?
If the answer is no, is there a good alternative for listening to changes to a specific entity (and its relationship)?
There's no way to listen to changes to a specific set of entities; catching NSManagedObjectContextObjectsDidChangeNotification (or did or will save) and filtering is the correct approach, with the caveat that key-value observing is also an option if you're talking about specific instances of entities.
However, worth observing is that NSManagedObjectID is thread-safe and provides a getter for the NSEntityDescription. So you could e.g.
- (void)handleDataModelChange:(NSNotification *)note {
NSSet *updatedObjects = [[note userInfo] objectForKey:NSUpdatedObjectsKey];
NSSet *deletedObjects = [[note userInfo] objectForKey:NSDeletedObjectsKey];
NSSet *insertedObjects = [[note userInfo] objectForKey:NSInsertedObjectsKey];
NSMutableArray *objectIDs = [NSMutableArray array];
[objectIDs addObjectsFromArray:[updatedObjects.allObjects valueForKey:#"objectID"]];
[objectIDs addObjectsFromArray:[deletedObjects.allObjects valueForKey:#"objectID"]];
[objectIDs addObjectsFromArray:[insertedObjects.allObjects valueForKey:#"objectID"]];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(){
NSSet *affectedEntities = [NSSet setWithArray:[objectIDs valueForKeyPath:#"entity.name"]];
if([affectedEntities containsObject:#"InterestingEntity"]) {
// get back onto the main thread and do some proper work;
// possibly having collated the relevant object IDs here
// first — whatever is appropriate
}
});
}
After reading through the docs and some other forms I also came up with another possible solution.
Why not override the -(void) didSave method of the managed object to catch a change on that object.
#implementation Report (helper)
- (void) didSave {
[super didSave];
// if not on default context no-op
if ([NSManagedObjectContext MR_defaultContext] != self.managedObjectContext) return;
// send custom notification here
}
#end
Still deals with sending a custom notification, but at least in encapsulated in the managed object, such that devs don't have to worry about where to send the notification.
On the plus side this only executes for the managed object that I care about.
Minus is that it is called for all managed object contexts. However I don't think the MOC check is going to be a heavy hitter.
Not sure if this is better/worse than listening on the default MOC for saves and filtering. Plus in that case is that I know I am listening for saves only on that MOC. Even though I can filter in a background task I am still filtering tons of data in that case that I don't need to deal with.
Thoughts?
I want my app to check the core data store at start-up. If the store is empty, it will add two items into it. What is the best way to implement this?
Can I put the following code in viewDidLoad?
- (void)viewDidLoad
{
[super viewDidLoad];
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"MonitorItem"];
self.monitorItemArray = [[managedObjectContext
executeFetchRequest:fetchRequest
error:nil] mutableCopy];
// If the core data is empty, populate it with the two compulsory items
if ([self.monitorItemArray count] == 0)
{
self.AddMandatoryItems;
}
[self.tableView reloadData];
}
I have searched other articles but none seems to give me an answer that I could understand.
Getting information from managedObjectContext in viewDidLoad is good.
If in cellForRowAtIndexPath you populate the cells from self.monitorItemArray than there is no reason to call reloadData (Which essentially erase the entire table view and re-draw it from scratch - which is exactly what happens when the view is appearing on screen any way...).
If you also show information from a web service, you can call reloadData in the response method to replace the existing data with the one that came from the web. Otherwise - if only information from core data is shown on the table view - no need for reloadData (Or maybe only in a case where the information in your managedObjectContext has changed while the table view is on screen).
I have a UITableViewController that's a subclass of CoreDataTableViewController (it's the Stanford class). That implements a fetchedResultsController.
Now, in my viewWillAppear, I have this:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if(!self.managedObjectContext) {
[self useManagedDocument];
}
}
It initializes the managedObjectContext if I don't have one, and gets it from a helper class. In the MOC setter, I initialize the fetchedResultsController:
- (void)setManagedObjectContext:(NSManagedObjectContext *)managedObjectContext
{
_managedObjectContext = managedObjectContext;
if(self.managedObjectContext) {
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:CD_ITEM_ENTITY_NAME];
request.sortDescriptors = #[[NSSortDescriptor
sortDescriptorWithKey:CD_ITEM_NAME_PROPERTY
ascending:YES
selector:#selector(localizedCaseInsensitiveCompare:)]];
request.predicate = nil;
self.fetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:request
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
} else {
self.fetchedResultsController = nil;
}
}
When my program starts, it loads the table data up correctly and my debugger says there was a fetch request made. However, after inserting data into my Core Data graph, and saving, it says the context changes and fires this delegate method:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
/*NSError *error;
[controller performFetch:&error];
if(error) {
NSLog(#"%#", error);
}
[self.tableView reloadData];*/
}
I commented this because it wasn't working. Basically, what I want to do is reload the data every time the context changes. If I add an item in another view controller and then go back to this one, it should reload in that case too.
How do I implement this? I tried doing performFetch in that delegate method and it entered it (I checked by setting a breakpoint inside), but the performFetch did nothing and my table wasn't reloaded.
When I add an item in a modal VC (another one I have for managing items) this is what happens in my logger:
2013-05-10 22:41:38.264 App1[7742:c07] [ItemCDTVC performFetch] fetching all Item (i.e., no predicate)
2013-05-10 22:41:46.454 App1[7742:c07] NSManagedObjects did change.
2013-05-10 22:41:46.456 App1[7742:c07] NSManagedContext did save.
When I close my app but do not quit it from the multitasking bar, and then reopen it, it does nothing. No fetch. Well, if the context didn't change I don't want it to fire a request, but imagine if the user adds an item in another ViewController and then goes back to my ItemCDTVC, which lists all items. Does it get a context changed notification so it can call the delegate method to update the table, or will I always have to refresh regardless of changes in my viewWillAppear? I currently have it set to do it only once, on app load.
Fixed, all I had to do is put a one liner in that delegate method:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
This ends updates, inserts, deletes, and changes made to the table (refreshing my view, essentially) the same as per Apple's documentation.
It now updates on content change.
I have a problem with a UITableView. I load data from a database server in a background thread and if it is finished it sends a notification. In the notification I update the data array of my view and use reloadData on the tableview.
Then the tableview deselects the selected row (that means the data is reloaded) and if I want to select another row I get EXC_BAD_ACCESS on the first line of didSelectRowAtIndexPath, even if it just is an NSLog.
If I don't assign the new array the background thread gives me to the variable data and don't use reloadData I have no problems with didSelectRowAtIndexPath but the tableview doesn't show the recent records. The user had to close the view and open it again to see the changes. That is really bad and I wanted to show the changes immediatly after the background thread finished to load the records from the server.
declared variables in the .h file:
-downloadThread is an NSThread,
-data is an NSArray,
-manager is my SQL interface.
-(void)viewDidLoad
{
...
[super viewDidLoad];
NSMutableArray *arr = [[manager getTeilnehmerList] retain];
data = arr;
[self.tableView reloadData];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(KundenUpdated:) name:#"ContactUpdate" object:nil]; // to be notified when updating thread is finished
downloadThread = [[NSThread alloc] initWithTarget:self selector:#selector(teilnehmerLoad:) object:nil]; //Thread to get actual data on Background
[downloadThread performSelector:#selector(start) withObject:nil afterDelay:2];
...
}
-(void)teilnehmerLoad:(id)sender
{
[manager loadTeilnehmerFromServerAndInsertIntoDatabase];
//data = [manager getTeilnehmerList];
[self.tableView reloadData];
[[NSNotificationCenter defaultCenter] postNotificationName:#"ContactUpdate" object:nil];
}
-(void)KundenUpdated:(NSNotification*)notifaction
{
#synchronized(self)
{
//needs function to select row that was selected before reload if the data record is still there after sync with server
[self.tableView reloadData];
NSLog(#"count data in kundenupdated: %i",data.count);
}
}
I suspect that teilnehmerLoad is called in your background thread, and it's calling reloadData, which is a no-no.
Change it (and/or KundenUpdated) to use performSelectorOnMainThread to do the reloadData.
Make sure you're not doing any other UI operations from your background thread.
I need some help in using objects from Core Data with GCD; I seem to get NSManagedObjects that are aren't faulted into the main thread, even when I access their properties. Would appreciate some help.
This is what I'm doing: on launch, I need to load a list of Persons from the Core Data DB, do some custom processing in the background, then reload the table to show the names. I am following the guidelines for Core Data multi-threading by only passing in the objectIDs into the GCD queues. But when I reload the tableview on the main thread, I never see the name (or other properties) displayed for the contacts, and on closer inspection, the NSManagedObjects turn out to be faults on the main thread, even though I access various properties in cellForRowAtIndexPath. The name property is visible in the background thread when I NSLog it; and it's also showing correctly on the main thread in NSLogs in cellForRowAtIndexPath. But they don't show in the tableView no matter what I do. I tried accessing the name property using the dot notation, as well as valueForKey, but neither worked.
Here's my code …. it's called from the FRC initializer:
- (NSFetchedResultsController *)fetchedResultsController
{
if (__fetchedResultsController != nil)
{
return __fetchedResultsController;
}
__fetchedResultsController = [self newFetchedResultsControllerWithSearch:nil]; // creates a new FRC
[self filterAllContactsIntoDictionary: __fetchedResultsController];
return [[__fetchedResultsController retain] autorelease];
}
- (void) filterAllContactsIntoDictionary: (NSFetchedResultsController *) frc
{
NSArray *fetchedIDs = [[frc fetchedObjects] valueForKey:#"objectID"];
NSArray *fetched = [frc fetchedObjects];
if (filterMainQueue == nil) {
filterMainQueue = dispatch_queue_create("com.queue.FilterMainQueue", NULL);
}
dispatch_async(self.filterMainQueue, ^{
NSManagedObjectContext *backgroundContext = [[[NSManagedObjectContext alloc] init] autorelease];
[backgroundContext setPersistentStoreCoordinator:[[self.fetchedResultsController managedObjectContext] persistentStoreCoordinator]];
NSMutableArray *backgroundObjects = [[NSMutableArray alloc] initWithCapacity: fetchedIDs.count];
// load the NSManagedObjects in this background context
for (NSManagedObjectID *personID in fetchedIDs)
{
Person *personInContext = (Person *) [backgroundContext objectWithID: personID];
[backgroundObjects addObject:personInContext];
}
[self internal_filterFetchedContacts: backgroundObjects]; // loads contacts into custom buckets
// done loading contacts into character buckets ... reload tableview on main thread before moving on
dispatch_async(dispatch_get_main_queue(), ^{
CGPoint savedOffset = [self.tableView contentOffset];
[self.tableView reloadData];
[self.tableView setContentOffset:savedOffset];
});
});
}
What am I doing wrong here? Is there any other way to explicitly make the Person objects fire their faults on the main thread? Or am I doing something wrong with GCD queues and Core Data that I'm not aware of?
Thanks.
Why not take the easy route, since you are not saving anything new ?
Instead of creating an extra context for the background thread and working with IDs, use the main managedObjectContext in the background thread after locking it.
for example:
- (void) filterAllContactsIntoDictionary: (NSFetchedResultsController *) frc
{
if (filterMainQueue == nil) {
filterMainQueue = dispatch_queue_create("com.queue.FilterMainQueue", NULL);
}
dispatch_async(self.filterMainQueue, ^{
NSManagedObjectContext *context = ... // get the main context.
[context lock]; // lock the context.
// do something with the context as if it were on the main thread.
[context unlock]; // unlock the context.
dispatch_async(dispatch_get_main_queue(), ^{
CGPoint savedOffset = [self.tableView contentOffset];
[self.tableView reloadData];
[self.tableView setContentOffset:savedOffset];
});
});
}
This works for me when I call a method with performSelectorInBackground, so I guess it should work for GCD dispatch too.
Well, mergeChangesFromContextDidSaveNotification: is your friend. You'll need to tell the MOC on the main thread that there have been changes elsewhere. This will do the trick.
Here's Apple's documentation. To quote from there:
This method refreshes any objects which have been updated in the other context, faults in any newly-inserted objects, and invokes deleteObject:: on those which have been deleted.
EDIT: original answer removed, OP is not fetching in the background
I looked closer at your code and it doesn't look like you are doing anything that will change data and/or affect the context on the main thread.
You have a fetchedResultsController on the main thread. Presumably, this is working and your table is populating with data. Is this true?
When filterAllContentsIntoDictionary is invoked, you pass an array of the fetchedResultsController's current objectIDs to a background thread and do some processing on them (presumably filtering them based on some criteria) but you are not changing data and saving backgroundContext.
internalFilterFetchedContents is a black box. Without knowing what you intend for it to do, hard to say why it's not working.
When this is done, you reload the table on the main thread.
You haven't made any changes to the store, the context, or the fetchedResultsController so of course, the table shows the same data it did before. The missing details to help further are:
Is your tableView showing correct data from the fetchedResultsController to begin with? If not, most likely your only problem is in handling the tableView delegate and dataSource methods and the rest of this isn't really relevant.
What do you intend to happen in filterAllContentsIntoDictionary and internalFilterFetchedContents?
If your intent is to filter the data as displayed by the fetchedResultsController not sure you need to do anything in the background. If you modify the fetchRequest and do performFetch again your table will reload based on the new results.
I you need more help, please answer my questions, add more relevant code to your post and let me know if I'm missing anything wrt the problem and what you're trying to accomplish.
good luck!