I am trying to create background threads for fetching Core Data in order to populate an NSOutlineView. The operation can take a few seconds so I was wanting to put the operation in a background thread and display the UI to the user as shown below. As the searches complete for each section I am calling a function on the main thread to reload the data for that section. This seemed to be working OK but I have had one instance where an exception was thrown during the Core Data fetch operation. The user can't really do anything else so i figured it was probably OK to do Core Data calls on a background thread as long as nothing else can do so at the same time. Subsequently I have used NSLock's around the code doing updates to the array that holds the data for display. I can't seem to cause any exceptions now. Is this a safe way to retrieve data from a managedObjectContext ?
The basic approach I have used is illustrated in the code snippets below and is as follows:
1. In initWithNibName
populate the initial data for display
2. In awakeFromNib
setup the outlineView and kick of the background thread to fetch the first set of data
3. Run the background thread and block while updating shared objects
4. When the thread is done trigger execution of a function on the main thread to a) reload the outlineView (wish I knew how to only reload the section of NSOutlineView), and b) kick of the background thread for the next section's fetch.
In general this seems to work OK and seems to create a better visual experience for the user than simply running it all on the main thread and watching the coloured umbrella spinning for a few seconds...
Can anyone confirm whether this is a robust approach - I am not sure I fully understand what NSLock does and I suspect there may be some holes in this approach but can't seem to break it.
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Initialization code here.
_reminderCategories = [NSArray arrayWithObjects:REMINDERS_PAST, REMINDERS_TODAY, REMINDERS_THIS_WEEK, REMINDERS_THIS_MONTH, REMINDERS_THIS_YEAR, nil];
_reminderItems = [NSMutableDictionary new];
[_reminderItems setObject:[NSArray arrayWithObjects:@"Searching...", nil] forKey:REMINDERS_PAST];
[_reminderItems setObject:[NSArray arrayWithObjects:@"Searching...", nil] forKey:REMINDERS_TODAY];
[_reminderItems setObject:[NSArray arrayWithObjects:@"Searching...", nil] forKey:REMINDERS_THIS_WEEK];
[_reminderItems setObject:[NSArray arrayWithObjects:@"Searching...", nil] forKey:REMINDERS_THIS_MONTH];
[_reminderItems setObject:[NSArray arrayWithObjects:@"Searching...", nil] forKey:REMINDERS_THIS_YEAR];
}
return self;
}
- (void)awakeFromNib;
{
// Make sure we only do this once
if (!_awake) {
_awake = YES;
LOG(@"awakeFromNib called !");
//set the start menu option
[_sidebarOutlineView sizeLastColumnToFit];
[_sidebarOutlineView reloadData];
[_sidebarOutlineView setFloatsGroupRows:NO];
// Expand all the root items; disable the expansion animation that normally happens
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:0];
[_sidebarOutlineView expandItem:nil expandChildren:YES];
[NSAnimationContext endGrouping];
if (!_searched) {
_searched=YES;
[self getPastRemindersUsingThread];
}
}
}
- (void)getPastRemindersUsingThread
{ //LOG(@"getPastRemindersUsingThread called");
_loading = YES;
[self performSelectorInBackground:@selector(threadGetPastReminders) withObject:nil];
}
- (void)threadGetPastReminders
{
//LOG(@"threadGetReminders called");
assert( ! [NSThread isMainThread] );
[NSThread sleepForTimeInterval:2]; // Delays for testing
[self makeMenus];
[self performSelectorOnMainThread:@selector(pastRemindersDone) withObject:nil waitUntilDone:NO];
}
- (void)pastRemindersDone {
//LOG(@"pastRemindersDone called");
[_sidebarOutlineView reloadData];
[self getTodaysRemindersUsingThread];
}
- (void)getTodaysRemindersUsingThread
{ //LOG(@"getTodaysRemindersUsingThread called");
_loading = YES;
[self performSelectorInBackground:@selector(threadGetReminders) withObject:nil];
}
- (void)threadGetReminders
{
//LOG(@"threadGetReminders called");
assert( ! [NSThread isMainThread] );
[self makeTodaysMenusX];
[self performSelectorOnMainThread:@selector(todaysRemindersDone) withObject:nil waitUntilDone:NO];
}
- (void)todaysRemindersDone {
//LOG(@"todaysRemindersDone called");
[_sidebarOutlineView reloadData];
[self getThisWeeksRemindersUsingThread];
}
// etc…
-(void)makeMenus {
//LOG(@"makeMenus called");
// Make sure nothing else is going on while we execute this code
NSLock *theLock = [[NSLock alloc] init];
if ([theLock tryLock]) {
[self makeDates];
[self getAllReminders];
[self makePastMenus];
}
[theLock unlock];
}
- (void)makePastMenus {
//LOG(@"makePastMenus called");
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"nextReminderDate < %@",_todayStartDate];
[self makeMenu:REMINDERS_PAST predicate:predicate];
}
- (void)makeTodaysMenus {…}
- (void)makeThisWeeksMenus {…}
- (void)makeThisYearsMenus {…}
// Use this to get the subset that fall within the specified dates (in the predicate)
// nextReminderDate is calculated, and not an attribute stored in the database
- (void)makeMenu:(NSString*)menu predicate:(NSPredicate*)predicate {
LOG(@"makeMenuX called");
NSLock *theLock = [[NSLock alloc] init];
if ([theLock tryLock]) {
if (_yearlyReminders) {
NSMutableArray *reminders = [[NSMutableArray alloc] init];
[reminders addObjectsFromArray:[_yearlyReminders filteredArrayUsingPredicate:predicate]];
NSSortDescriptor *indexSort = [[NSSortDescriptor alloc] initWithKey:@"nextReminderDate" ascending:YES];
NSArray *sorters = [NSArray arrayWithObject:indexSort]; indexSort = nil;
[_reminderItems setObject:[reminders sortedArrayUsingDescriptors:sorters] forKey:menu];
} else {
LOG(@" error utilities is nil!");
}
}
[theLock unlock];
LOG(@"makePastYearsMenus finished");
}
// Get all the reminders from the Core Data store
// _utilities getData is a helper function that performs the Core Data fetch and returns an array of NSManagedObjects
- (void)getAllReminders {
//LOG(@"getYearlyReminders called");
NSLock *theLock = [[NSLock alloc] init];
if ([theLock tryLock]) {
if (_utilities) {
NSPredicate *predicate = [...];
NSMutableArray *reminders = [[NSMutableArray alloc] init];
[reminders addObjectsFromArray:[_utilities getData:@"Entity1" sortKey:@"reminderDate" predicate:predicate]];
[reminders addObjectsFromArray:[_utilities getData:@"Entity2" sortKey:@"reminderDate" predicate:predicate]];
[reminders addObjectsFromArray:[_utilities getData:@"Entity3" sortKey:@"reminderDate" predicate:predicate]];
[reminders addObjectsFromArray:[_utilities getData:@"Entity4" sortKey:@"reminderDate" predicate:predicate]];
NSSortDescriptor *indexSort = [[NSSortDescriptor alloc] initWithKey:@"nextReminderDate" ascending:YES];
NSArray *sorters = [NSArray arrayWithObject:indexSort]; indexSort = nil;
_yearlyReminders = [[NSArray alloc] initWithArray:[reminders sortedArrayUsingDescriptors:sorters]];
} else {
LOG(@" error utilities is nil!");
}
}
[theLock unlock];
}