NSFetchedResultsController attempting to insert nil object - ios

Edit 7:
Here's my save method. It's pretty boilerplate. The DEBUG_LOG() macros are only executed if it's a debug build.
- (void)saveManagedObjectContext:(NSManagedObjectContext *)moc
{
if ([moc hasChanges]) {
DEBUG_LOG(#"Saving managed object context %#", moc);
NSError *error;
BOOL success = [moc save:&error];
if (!success || error) {
DEBUG_LOG(#"ERROR: Couldn't save to managed object context %#: %#",
moc, error.localizedDescription);
}
DEBUG_LOG(#"Finished saving managed object context %#", moc);
} else {
DEBUG_LOG(#"Managed object context %# had no changes", moc);
}
}
Edit 6:
iOS 8 is here and this problem is back. Lucky me. Previously I had narrowed the problem down to using estimatedRowHeight on table views (btw, I never fully fixed the problem. I just stopped using estimatedRowHeight). Now I'm seeing this problem again under different circumstances. I tracked it down to a commit from a few days ago when I made my nav/tab bars translucent. This included disabling 'Adjust Scroll View Insets' in storyboard and checking the boxes to have my views display under top bars and bottom bars. There's a series of steps I need to do to make it happen, but I can reproduce it every time with my storyboard configured that way. If I revert that commit it no longer happens.
While I say, "it no longer happens," what I really think is that this is just making it less likely to happen. This bug is an absolute b****. My gut reaction now is that this is an iOS bug. I just don't know what I can do to turn this into a bug report. It's madness.
Edit 5:
If you want to read the entirety of my misery, please continue all the way through this post. If you're running into this problem and you just want some help, here's something to look into.
My last edit noted that when I used a basic table view cell, everything worked fine. My next course of action was going to be to try from scratch building a new custom cell piece by piece and seeing where it messed up. For the hell of it, I re-enabled my old custom cell code and it worked just fine. Uhhh? Oh wait, I still have estimatedHeightForRowAtIndexPath commented out. When I removed those comments and enabled estimatedHeightForRowAtIndexPath, it got crappy again. Interesting.
I looked up that method in the API doc, and it mentioned something about a constant called UITableViewAutomaticDimension. The value I was estimating was really just one of the common cell heights, so it wouldn't hurt to switch to that constant. After switching to that constant it's working properly. No weird exceptions/graphical glitches to report.
Original post
I have a pretty standard iPhone app that fetches data from a web service in the background and displays data in a table view. The background updating work has its own managed object context configured for NSPrivateQueueConcurrencyType. My table view's fetched results controller has its own managed object context configured for NSMainQueueConcurrencyType. When the background context parses new data it passes that data to the main context via mergeChangesFromContextDidSaveNotification. Sometimes during the merge, my app hits an exception here...
Thread 1, Queue : com.apple.main-thread
#0 0x3ac1b6a0 in objc_exception_throw ()
#1 0x308575ac in -[__NSArrayM insertObject:atIndex:] ()
#2 0x33354306 in __46-[UITableView _updateWithItems:updateSupport:]_block_invoke687 ()
#3 0x330d88d2 in +[UIView(UIViewAnimationWithBlocks) _setupAnimationWithDuration:delay:view:options:factory:animations:start:animationStateGenerator:completion:] ()
#4 0x330ef7e4 in +[UIView(UIViewAnimationWithBlocks) animateWithDuration:delay:options:animations:completion:] ()
#5 0x3329e908 in -[UITableView _updateWithItems:updateSupport:] ()
#6 0x332766c6 in -[UITableView _endCellAnimationsWithContext:] ()
#7 0x0005ae72 in -[ICLocalShowsTableViewController controllerDidChangeContent:] at ICLocalShowsTableViewController.m:475
#8 0x3069976c in -[NSFetchedResultsController(PrivateMethods) _managedObjectContextDidChange:] ()
#9 0x308dfe78 in __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ ()
#10 0x30853b80 in _CFXNotificationPost ()
#11 0x3123a054 in -[NSNotificationCenter postNotificationName:object:userInfo:] ()
#12 0x306987a2 in -[NSManagedObjectContext(_NSInternalNotificationHandling) _postObjectsDidChangeNotificationWithUserInfo:] ()
#13 0x306f952a in -[NSManagedObjectContext _mergeChangesFromDidSaveDictionary:usingObjectIDs:] ()
#14 0x306f9734 in -[NSManagedObjectContext mergeChangesFromContextDidSaveNotification:] ()
#15 0x0006b5be in __65-[ICManagedObjectContexts backgroundManagedObjectContextDidSave:]_block_invoke at ICManagedObjectContexts.m:133
#16 0x306f9854 in developerSubmittedBlockToNSManagedObjectContextPerform ()
#17 0x3b1000ee in _dispatch_client_callout ()
#18 0x3b1029a8 in _dispatch_main_queue_callback_4CF ()
#19 0x308e85b8 in __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ ()
#20 0x308e6e84 in __CFRunLoopRun ()
#21 0x30851540 in CFRunLoopRunSpecific ()
#22 0x30851322 in CFRunLoopRunInMode ()
#23 0x355812ea in GSEventRunModal ()
#24 0x331081e4 in UIApplicationMain ()
#25 0x000554f4 in main at main.m:16
Here's the exception I see...
CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. *** -[__NSArrayM insertObject:atIndex:]: object cannot be nil with userInfo (null)
My app is actually hitting the exception in controllerDidChangeContent, at my call to endUpdates. I'm basically seeing the same thing as this (NSFetchedResultsController attempting to insert nil object?), but I've got more info and case a that's reproducible. All of my merge events are inserts. During the merge, there doesn't seem to be any pending inserts, deletes, or updates on the background context. I was initially using performBlockAndWait all over the place until I learned about the difference between performBlock and performBlockAndWait from the WWDC video. I switched to performBlock, and that made it a little bit better. Initially I approached this as a threading issue, diverged into the possibility of it being a weird memory problem caused by not fully understanding blocks, and now I'm back to it being a race condition. It seems like there's just one piece that I'm missing. There are two ways it doesn't happen...
(1) Register for the context will save notification, nil out the FRC delegate when I get it, and set the delegate back after the merge. This isn't far from not using an FRC at all, so this really isn't an option for a workaround.
(2) Do things that block the main thread long enough, so the race condition doesn't happen. For example, when I add a lot of debug log messages to my table view delegate, that slows it down enough for it not to happen.
Here are what I believe to be the important pieces of code (I've shortened certain spots to shrink this already large post).
After various points during scrolling, the view controller will request more data by calling a function that has this in it...
AFJSONRequestOperation *operation =
[AFJSONRequestOperation JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
// Parsing happens on MOC background queue
[backgroundMOC performBlock:^ {
[self parseJSON:JSON];
// Handle everything else on the main thread
[mainMOC performBlock:^ {
if (completion) {
// Remove activitiy indicators and such from the main thread
}
}];
}];
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
[[NSOperationQueue mainQueue] performBlock:^ {
if (completion) {
// Remove activitiy indicators and such from the main thread
}
// Show an alert view saying that the request failed
}];
}
];
[operation setCacheResponseBlock:^NSCachedURLResponse *(NSURLConnection *connection, NSCachedURLResponse *cachedResponse) {
return nil;
}];
[_operationQueue addOperation:operation];
For the most part, parseJSON doesn't really have anything interesting in it...
- (void)parseJSON:(NSDictionary *)json
{
NSError *error;
NSArray *idExistsResults;
NSNumber *eventId;
NSFetchRequest *idExistsFetchRequest;
LastFMEvent *event;
NSManagedObjectModel *model = backgroundMOC.persistentStoreCoordinator.managedObjectModel;
for (NSDictionary *jsonEvent in jsonEvents) {
eventId = [NSNumber numberWithInt:[jsonEvent[#"id"] intValue]];
idExistsFetchRequest = [model fetchRequestFromTemplateWithName:kGetEventByIDFetchRequest substitutionVariables:#{#"eventID" : eventId}];
idExistsResults = [backgroundMOC executeFetchRequest:idExistsFetchRequest error:&error];
// Here I check for errors - omitted that part
if ([idExistsResults count] == 0) {
// Add a new event
event = [NSEntityDescription insertNewObjectForEntityForName:[LastFMEvent entityName] inManagedObjectContext:backgroundMOC];
[event populateWithJSON:jsonEvent];
} else if ([idExistsResults count] == 1) {
// Get here if I knew about the event already, so I update a few fields
}
}
[self.mocManager saveManagedObjectContext:backgroundMOC];
}
The implementation for save and merge are where it might get interesting. Save expects to be called from within the appropriate performBlock already, so it doesn't do anything with the performBlock.
- (void)saveManagedObjectContext:(NSManagedObjectContext *)moc
{
if ([moc hasChanges]) {
NSError *error;
BOOL success = [moc save:&error];
if (!success || error) {
NSLog(#"ERROR: Couldn't save to managed object context %#: %#",
moc, error.localizedDescription);
}
}
}
Upon saving, the merge notification gets triggered. I'm only merging from background to main, so I pretty much just want to know if I can inline the merge call or if I need to do it inside of performBlock.
- (void)backgroundManagedObjectContextDidSave:(NSNotification *)notification
{
if (![NSThread isMainThread]) {
[mainMOC performBlock:^ {
[self.mainMOC mergeChangesFromContextDidSaveNotification:notification];
}];
} else {
[mainMOC mergeChangesFromContextDidSaveNotification:notification];
}
}
My fetched results controller delegate methods are pretty boiler plate stuff...
- (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:#[newIndexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:(ICLocalShowsTableViewCell *)[tableView cellForRowAtIndexPath:indexPath]
atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:#[newIndexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeSection:(id )sectionInfo
atIndex:(NSUInteger)sectionIndex
forChangeType:(NSFetchedResultsChangeType)type
{
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
}
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
One other piece of code that might be of interest. I'm using autolayout for my table view cells, and the new estimatedHeightForRowAtIndexPath API for dynamic cell height. What this means is that during the call to [self.tableView endUpdates], the last step actually reaches down into some managed objects, whereas the other calls for number of sections/rows only need to know counts from the FRC.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSAssert([NSThread isMainThread], #"");
LastFMEvent *event = [self.fetchedResultsController objectAtIndexPath:indexPath];
if (!_offscreenLayoutCell) {
_offscreenLayoutCell = [self.tableView dequeueReusableCellWithIdentifier:kLocalShowsCellIdentifier];
}
[_offscreenLayoutCell configureWithLastFMEvent:event];
[_offscreenLayoutCell setNeedsLayout];
[_offscreenLayoutCell layoutIfNeeded];
CGSize cellSize = [_offscreenLayoutCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
return cellSize.height;
}
Been stuck on this for almost a week now. Learned a ton in the process, but geez I'm ready to move on. Any suggestions would be greatly appreciated.
Edit
I put together a pretty big debug log to try to tell the story of what's going on with the udpates. I'm seeing something really strange. I'm updating the table with 50 rows at a time, so I'll only include the interesting part of my debug output. Every time a cell gets configured I'm printing out what the title was for the cell that I just dequeued as well as what the new title will be. When I hit the last cell in the table view, I make a query to the web service for more data. This output is related to the final update before I hit the exception...
// Lots of output was here that I omitted
configure cell at sect 5 row 18 WAS Suphala NOW Keller Williams
configure cell at sect 5 row 19 WAS Advocate Of Wordz NOW Gates
configure cell at sect 5 row 20 WAS Emanuel and the Fear NOW Beats Antique
configure cell at sect 5 row 21 WAS The Julie Ruin NOW Ashrae Fax
// At this point I hit the end of the table and query for more data - for some reason row 18 gets configured again. Possibly no big deal.
configure cell at sect 5 row 18 WAS Keller Williams NOW Keller Williams
configure cell at sect 5 row 22 WAS Old Wounds NOW Kurt Vile
JSON size 100479
Starting JSON parsing
page 3 of 15. total events 709. events per page 50. current low idx 100 next trigger idx 149
// Parsing data finished, saving background context
Saving managed object context <NSManagedObjectContext: 0x17e912f0>
Background context will save
Finished saving managed object context <NSManagedObjectContext: 0x17e912f0>
Merging background context into main context
JSON parsing finished
** controllerWillChangeContent called **
** BEGIN UPDATES triggered **
inserting SECTION 6
inserting SECTION 7
inserting SECTION 8
inserting ROW sect 5 row 17
inserting ROW sect 5 row 22
inserting ROW sect 5 row 25
inserting ROW sect 5 row 26
inserting ROW sect 5 row 27
inserting ROW sect 5 row 28
inserting ROW sect 5 row 29
// A bunch more rows added here that I omitted
** controllerDidChangeContent called **
// This configure cell happens before the endUpdates call has completed
configure cell at sect 5 row 18 WAS Conflict NOW Conflict
In the final update it's attempting to insert at s5 r17, but I already had a cell at that row. It also attempts to insert at s5 r22, but I also already had a cell at that row. Lastly it inserts a row at s5 r25, which actually is a new row. It seems to me as though considering r17 and r22 as inserts is leaving a gap in the table. Shouldn't the previous cells at those indexes have events to be moved to r23 and r24?
My fetched results controller is using a sort descriptor that sorts by date and start time. Maybe the existing events that were at r17 and r22 aren't getting move events because there weren't any changes related to their NSManagedObjects. Essentially, they are required to move because of my sort descriptor for events earlier than them and not because their data changed.
Edit 2:
Looks like those inserts do just trigger the existing cells to shift down :(
Edit 3:
Things I tried today...
Made AFNetworking success block waits for the merge to complete before it returns
Made cellForRowAtIndexPath return a stale cell (essentially dequeue it and return it right away) if the fetched results controller is in the middle of beginUpdates/endUpdates. Thinking that extra random cellForRowAtIndexPath that gets called during the update may have been doing weird things.
Removing the background context altogether. This is interesting. If I do all of the UI updates AND JSON parsing on the main context, it still happens.
Edit 4:
Now it's getting interesting.
I tried removing random components in my table view such as the refresh control. Also tried getting rid of my use of estimatedHeightForRowAtIndexPath, which meant just supplying a static row height instead of using autolayout to determine dynamic row height. Both of those turned up nothing. I also tried getting rid of my custom cell entirely, and just using a basic table view cell.
That worked.
I tried a basic table view cell with subtitle.
That worked.
I tried a basic table view cell with subtitle and image.
That worked.
The top of my stack trace being near all of those animation related items is starting to make more sense. It's looking like this is auto-layout related.

From an Apple Technical Support Engineer:
To protect the integrity of the datastore, Core Data catches some
exceptions that happen during its operations. Sometimes this means
that if Core Data calls your code through a delegate method, Core Data
may end up catching exceptions your code threw.
Multi-threading errors are the most common cause of mysterious Core Data issues.
In this case, Core Data caught an exception through your controllerDidChangeContent: method, caused by trying to use insertObject:atIndex.
The most likely fix is to ensure that all your NSManagedObject code is encapsulated inside performBlock: or performBlockAndWait: calls.
In iOS 8 and OSX Yosemite, Core Data gains the ability to detect and report violations of its concurrency model. It works by throwing an exception whenever your app accesses a managed object context or managed object from the wrong dispatch queue. You enable the assertions by passing -com.apple.CoreData.ConcurrencyDebug 1 to your app on the command line via Xcodeʼs Scheme Editor.
Ole Begemann has a great writeup of the new feature.

Related

iOS 10: NSFetchedResultsController + UICollectionView, SIGABRT on performBatchUpdates

I'm using this gist for FRC and UICollectionView. This was working fine till iOS 9.
Now, in iOS 10 sometimes my app crashes with SIGABRT signal crash at performBatchUpdates of collectionview. Even if the CollectionView escapes from crash it goes in coma with 1 or 2 cells.
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
// Checks if we should reload the collection view to fix a bug # http://openradar.appspot.com/12954582
if (self.shouldReloadCollectionView) {
[self.collectionView reloadData];
} else {
[self.collectionView performBatchUpdates:^{ // CRASH : Thread 1: signal SIGABRT
[self.blockOperation start];
} completion:nil];
}
}
Is this happening because of new upgraded functionality of UICollectionView? What's the fix?
After doing some research, found a fix for this. My app fetches data from a web server and inserts it using main thread.
I assumed that this signal is getting raised because of some kind of invalid data manipulation. As I doubted controllerDidChangeContent( main thread ) delegate is getting called as soon as the thread starts saving the data. [self.managedObjectContext save:&savingError];
This early invocation causes performBatchUpdates to indulge in data manipulation in the middle of the saving process which results in crash.
Putting the controllerDidChangeContent code inside dispatch_async fixed the crash and the coma state of CollectionView. I hope this helps someone.
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
dispatch_async(dispatch_get_main_queue(), ^{
// Checks if we should reload the collection view to fix a bug # http://openradar.appspot.com/12954582
if (self.shouldReloadCollectionView) {
[self.collectionView reloadData];
} else {
[self.collectionView performBatchUpdates:^{ // No crash :)
[self.blockOperation start];
} completion:nil];
}
});
}

Parsing NSBlockOperation's executionBlocks

I have a UITableView where each section contains a single row which each contain a UICollectionView. The UITableView is the data source and delegate to a Core Data database. Updates to the database call the NSFetchedResultsController's controller: didChangeObject: method which queues blocks to update the relevant collection as so:
switch (type) {
case NSFetchedResultsChangeInsert: {
[self.blockOperation addExecutionBlock:^{
[collectionView insertSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] ];
}];
break;
}...
I then want to execute the blocks here as a batch:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[collectionView performBatchUpdates:^{
[self.blockOperation start];
} completion:^(BOOL finished) {
// Do whatever
}];
}
This is using Ash Furrow's and Blake Watters' techniques for hooking up a core data database to a UICollectionView.
My question is, how do I get access to the correct collectionView object in -controllerdidChangeContent:? Each block within self.blockOperation has the relevant collectionView, but I don't know how to parse it out from the NSBlockOperation's executionBlocks property, or even if that's the best way to get at it. Ash and Blake's example only has one UICollectionView whereas I have many.
You don't want to use only a single block operation because you won't be able to separate the data afterwards. Instead, use one block operation per collection view (which is also per section). Add each to a dictionary with the associated index path as the key (check it one already exists and create / update as necessary). When you run the blocks, iterate the dictionary, get the collection view for the index path (key) and run the block operation (value).

Rapid row insertion into UITableView causes NSInternalInconsistencyException

I have a UITableView that sometimes has rapid insertions of new rows. The insertion of the new rows is handled by a notification observer listening for the update notification fired whenever the underlying data changes. I use a #synchronized block around all the data model changes and the actual notification post itself... hoping that each incremental data change (and row insertion) will be handled separately. However, there are times when this still fails. The exception will tell me that it expects 10 rows (based on the count from the data model), it previously had 8 rows, but the update notification only told it to insert a single row (as this is the first of two rapidly fired notifications).
I'm trying to understand how other people tend to handle these types of situations. How do other developers mitigate the problems of having multi-threaded race conditions between two table view update operations? Should I have a more secure lock that controls the update notifications (and why isn't #synchronized doing what it's supposed to)?
Any advice is greatly appreciated! Thanks.
Some pseudo-code:
My model class has a method like this, which gets called by other threads to append new rows to the table view:
- (void)addRow:(NSString *)data
{
#synchronized(self.arrayOfData)
{
NSInteger nextIndex = self.arrayData.count;
[self.arrayData addObject:data];
[NSNotificationCenter.defaultCenter postNotificationName:kDataUpdatedNotification object:self userInfo:#{#"insert": #[[NSIndexPath indexPathForRow:nextIndex inSection:0]]}];
}
}
My controller class has a method like this to accept the kDataUpdatedNotification notification and actually perform the row insertion:
- (void)onDataUpdatedNotification:(NSNotification *)notification
{
NSDictionary *changes = notification.userInfo;
[self.tableView insertRowsAtIndexPaths:changes[#"insert"] withRowAnimation:UITableViewRowAnimationBottom];
}
You're going to have this problem if you change your data model asynchronously with the main queue because your table view delegate methods are looking at the current state of the data model, which may be ahead of the inserts you've reported to the table view.
UPDATE
One solution is to queue your updates on a private queue and have that queue update your data model on the main queue synchronously (I have not tested this code):
#interface MyModelClass ()
#property (strong, nonatomic) dispatch_queue_t myDispatchQueue;
#end
#implementation MyModelClass
- (dispatch_queue_t)myDispatchQueue
{
if (_myDispatchQueue == nil) {
_myDispatchQueue = dispatch_queue_create("myDispatchQueue", NULL);
}
return _myDispatchQueue;
}
- (void)addRow:(NSString *)data
{
dispatch_async(self.myDispatchQueue, ^{
dispatch_sync(dispatch_get_main_queue(), ^{
NSInteger nextIndex = self.arrayData.count;
[self.arrayData addObject:data];
[NSNotificationCenter.defaultCenter postNotificationName:kDataUpdatedNotification object:self userInfo:#{#"insert": #[[NSIndexPath indexPathForRow:nextIndex inSection:0]]}];
});
});
}
The reason you need the intermediate dispatch queue is as follows. In the original solution (below), you get a series of blocks on the main queue that look something like this:
Add row N
Add row N+1
Block posted by table view for row N animation
Block posted by table view for row N+1 animation
In step (3), the animation block is out-of-sync with the table view because (2) happened first, which results in an exception (assertion failure, I think). So, by posting the add row blocks to the main queue synchronously from a private dispatch queue, you get something like the following:
Add row N
Block posted by table view for row N animation
Add row N+1
Block posted by table view for row N+1 animation
without holding up your worker queues.
ORIGINAL Solution still has issues with overlapping animations.
I think you'll be fine if you update your data model on the main queue:
- (void)addRow:(NSString *)data
{
dispatch_async(dispatch_get_main_queue(), ^{
NSInteger nextIndex = self.arrayData.count;
[self.arrayData addObject:data];
[NSNotificationCenter.defaultCenter postNotificationName:kDataUpdatedNotification object:self userInfo:#{#"insert": #[[NSIndexPath indexPathForRow:nextIndex inSection:0]]}];
});
}

iOS freezing and threading with detachNewThreadSelector

I am working on an application that was designed by someone else. The application is full of calls like this:
[NSThread detachNewThreadSelector:#selector(loadPhotoImage) toTarget:self withObject:nil];
and
DownloadPhotoThread *thread = [[DownloadPhotoThread alloc] initWithFriendArray:_friendsList delegate:self];
[self.arrDownloadThreads addObject:thread];
[thread start];
And randomly I will get into situations where the entire ui locks, and the phone/simulator no longer respond to touches. Just to be clear, the device never unfreezes. If I hit pause during a debug session, I see a thread is sitting on a start or detachNewThreadSelector line.
Today I was able to narrow down a cause when these locks happen. I just added the Zendesk form controller (found here: https://github.com/zendesk/zendesk_ios_sdk/blob/master/DropboxSampleApp/FormViewController.m)
Which has this code:
- (void) textViewDidChange:(UITextView *)textView
{
[self.tableView beginUpdates];
[self.tableView endUpdates];
}
- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row > 1) {
CGSize s = [description sizeThatFits:CGSizeMake(description.frame.size.width, 10000)];
float dh = MAX(s.height, 115);
description.frame = CGRectMake(description.frame.origin.x,
description.frame.origin.y,
description.frame.size.width,
dh);
return dh;
}
return 44.0;
}
I can easily reproduce the lockup condition by typing a character and return over and over into this text box. (this resizes the height of the table view)
In some cases I am able to prevent the lockup condition by wrapping the offender with a dispatch_async() block like this:
DownloadPhotoThread *thread = [[DownloadPhotoThread alloc] initWithFriendArray:_friendsList delegate:self];
[self.arrDownloadThreads addObject:thread];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[thread start];
});
But in other cases I can't because the offending code is in a complex library such as ASIHTTPRequest or XMPPFramework. Do you have any advice on what exactly is happening here?
Another thing. When I hit pause in this condition, this is what the main thread says:
Thread 1
com.apple.main-thread
0 objc_msgSend
....
25 UIApplicationMain
26 main
pausing and unpausing will always find main at some assembly instruction inside objc_msgSend.
Thanks for your help cuz I'm seriously stumped here.
It turns out that the crash was caused by a bug in https://github.com/zendesk/zendesk_ios_sdk/ . I've made a pull request and submitted to them for review.

Fetching Core Data objects in the background: objects not faulted

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!

Resources