Background
I've got an
NSOutlineView
that shows TrainingGroup entities.Each TrainingGroup represents a folder on the local machine.
The
NSOutlineView
is bound to anNSTreeController
with a fetch predicate ofIsTrained == 0
Each TrainingGroup can be assigned to a project.
Each TrainingGroup has many TrainingEntries that show a time worked on that file.
When the TrainingGroup is assigned to a project, the
IsTrained
is set toYES
.On assign to a project, all descendants are also assigned to that project and their
IsTrained
property is set toYES
too.Project column is bound to
projectTopLevel
property.
Example
The whole tree looks like this:
Name Project IsTrained
Users nil NO
John nil NO
Documents nil NO
Acme Project Acme Project YES
Proposal.doc Acme Project YES
12:32-12:33 Acme Project YES
13:11-13:33 Acme Project YES
... etc
Budget.xls Acme Project YES
Big Co Project Big Co Project YES
Deadlines.txt Big Co Project YES
Spec.doc Big Co Project YES
New Project nil NO
StartingUp.doc nil NO
Personal Stuff Personal YES
MyTreehouse.doc Personal YES
Movies nil NO
Aliens.mov nil NO
StepMom.mov nil NO
And the NSOutlineView would only see this:
Users nil NO
John nil NO
Documents nil NO
New Project nil NO
StartingUp.doc nil NO
Movies nil NO
Aliens.mov nil NO
StepMom.mov nil NO
If you assigned Movies to Personal, the view would now look like this:
Users nil NO
John nil NO
Documents nil NO
New Project nil NO
StartingUp.doc nil NO
Code
TrainingGroup.m
-(void)setProjectTopLevel:(JGProject *)projectToAssign {
[self setProjectForSelf:projectToAssign];
[self setProjectForChildren:projectToAssign];
}
-(void)setProjectForSelf:(JGProject *)projectToAssign {
[self setProject:projectToAssign];
}
-(void)setProjectForChildren:(JGProject *)projectToAssign {
for (TrainingGroup *thisTrainingGroup in [self descendants]) {
[thisTrainingGroup setProject:projectToAssign];
if(projectToAssign != nil) {
[thisTrainingGroup setIsTrainedValue:YES];
} else {
[thisTrainingGroup setIsTrainedValue:NO];
}
// Other code updating rules.
}
}
-(JGProject *)projectTopLevel {
return [self project];
}
-(NSSet *)untrainedChildren {
// Code that loops through all children returning those
// whose isTrained is NO. Omitted for brevity.
}
The Problem
As you can see above, I'm running all the project assignment code on the main thread currently.
When there are hundreds of time entries under each folder, my app becomes unresponsive.
Possible Solutions
1 Modal progress bar
The Approach
- Run project assignment on a background thread in separate context.
- Use standard Core Data merge into main context when finished.
- A modal sheet blocks any further activity until the project assignment has finished.
The Good
- The user gets immediate feedback on what's happening.
- The app remains responsive.
The Bad
- User can't do anything until the current assignment has finished.
2 Non Modal Spinner
The Approach
- Run project assignment on a background thread in separate context.
- Use standard Core Data merge into main context when finished.
- Show progress spinner alongside training group, indicating it's busy.
- On finish assigning, the training group disappears from the view.
The Good
- The user can do other stuff whilst their last action is being processed
- The app remains responsive. Kinda. See below.
The Bad
- In tests, I've seen a freeze of up to 3 seconds when the background context is merged into the main context.
- The view could update in the middle of the user doing something else, which might be annoying.
- Undo would be hard to implement.
3 Hide
The Approach
- Above, except the training group is removed on assign, and is set to be "In Progress" until assign has finished.
Good and Bad
- Same as above, except the ordering of the training groups would remain predicatable.
- Still large freeze on merge back into main context.
4 Improve performance
The Approach
- Keep the code as it is, running on the main thread.
- Improve performance so even with thousands of entries, the view only freezes for half a second max
The Good
- App remains responsive.
- Undo remains easy.
- Architecture remains simple.
The Bad
- As I understand it, against Apple's recommendations - intensive processing should not be done on the main thread
- Can I get the performance good enough? Unknown.
My Questions
As far as I can see, none of the options above are ideal.
1. Which is the best option?
2. Are there any other options?
3. What could I improve about my approach?