Saturday 20 February 2010

I don't want to be in control of my computer

I've been thinking a lot about the idea of being in control of one's computer. There's been a lot of talk about this since the release of the iPad, with some people deriding the device for not allowing the user to be in control. The people who say such things tend to take it as read that the user should control exactly what the computer does.

Today I was thinking about to-do applications. I use OmniFocus (mainly on my Mac, but also on the iPhone) to manage my to-do list, and I have to confess I'm not very disciplined with keeping it up to date. For those who are not familiar with OmniFocus, it is spectacularly flexible and can adapt to many different workflows. It provides tools to make perspectives — different views on your tasks — and the user is supposed to design these to suit their own chosen working style.

I'm also going to talk about GTD (Getting Things Done). It's a workflow for personal productivity, with a couple of disciplines that you have to do which make sure that the rest of the time, managing your task list takes as little mental effort as possible. It has features such as ensuring that for every project, there is one clear task that's the next action to do. OmniFocus is designed to support GTD as a workflow (along with others).

I find it hard to keep up to date with my task list in OmniFocus. I'll use it for a while, and then some urgent thing happens which causes me to abandon the discipline of looking at my available tasks at the start of the day, sorting my inbox first, and then processing the tasks in an order of my choice. The obvious solution is to put the urgent task into the system, take 20 minutes to sort the inbox, then start work and do the urgent task first. Why don't I do that? It doesn't seem much effort. Yet as soon as I avoid using the system once, it's no longer a trusted system containing everything I need to do. What I lack is that self-discipline to make sure that, whatever happens, the first thing I do each day is open OmniFocus. This is software that is meant to help me stay organised, and instead I'm avoiding using it.

One problem is OmniFocus's extreme flexibility. It is meant to fit in with whatever workflow the user wants. That is to say, the software responds to the user's desired methodology. The user is in control. That control, in this case, is a problem. When I turned to OmniFocus, I didn't have any task management workflow. I started using it because I wanted to get such a workflow. Thus, I was starting from scratch. In this case, I don't want the software promising to obey me in all things: I want to-do list software that beats me round the head until I work how it wants me to work. I want the software to control me.

Thus, I came up with an idea for how task management software would work for me, if it were designed exactly to my spec. When my computer was started up for the first time on a working day, the software would automatically go full-screen and take me through the various admin things to manage my to-do list. It would show me an inbox (combining my email inbox and also the task inbox I jot down notes for things to do later), and present the items in it one at a time. I would then have to turn the items into either specific actionable tasks, or data to save elsewhere. After doing that for each one, it archives the email and moves on. Next, I get to pick what projects I want to focus on that day. Finally, it goes out of full screen and presents one task it's picked, that I should work on next.

Recall I find it hard to be self-disciplined about keeping my task list up to date. This software solves that by not letting me use my computer until I'm up to date. Granted, I could force quit the app… but that requires admitting I've given up. It is so much easier to be lazy and not do something (i.e. not check OmniFocus first thing in a day) than do something explicit that means I'm admitting to failing (force quit a full screen to-do list app because I don't want to work).

After having thought of that key feature, I sat down and designed the rest of this hypothetical app. I won't go into too much detail here (possibly a future blog post), but it's all designed to not give the user too much of a choice in methodology. I tend to gravitate to software with that philosophy. If there's one clear way to do things, and I have to adapt to that workflow, I'm much more likely to like the software: after all, if I'm looking for new software, chances are I want to change the way I work. One single workflow shows that the developers have thought about the ideal way to perform the task, and I'd rather pay developers to think of that for me than think it up myself then work out how to make existing software fit it.

In a similar vein, recently I obtained remotely hosted version control for writing software. I could have installed Mercurial myself, but instead I went with a private repository on BitBucket. With this, I had to fill in a form, it created the repository, and then gave me instructions for how to access it. If I installed the software myself, I'd have to learn how to do it, worry about getting it right, and be the one who has to fix it if it breaks. But some people seemed surprised that I didn't do this — and the only reason they suggested for why it was better was "It's more flexible".

Sod flexibility. I'd rather one way of doing things that is 80% right than a flexible solution, where I can make it 95% right if I try but it'll take me hours to do so. I'll happily adapt my workflow, if it means I get a well designed piece of software that doesn't push too many choices on me.

Tuesday 2 February 2010

Reordering rows in a UITableView with Core Data

I've read a lot of things about how best to handle reorderable rows in a NSFetchedResultsController-backed UITableView, and I think I've hit upon a decent solution. This post only works with a single section table. (I may post some ideas about multi section stuff in a future post.)

Core Data doesn't have any native handling of user-reorderable objects. Like a database, it lets you apply a sort ordering to a query. So, to allow reordering, we need an attribute called sortOrder, which represents this user generated ordering.

In the viewDidLoad method of your UITableViewController subclass, set up your NSFetchedResultsController.


- (void)viewDidLoad
{
[super viewDidLoad];

NSManagedObjectContext *managedObjectContext = self.managedObjectContext;

NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"sortOrder" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];

NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
[fetchRequest setEntity:[NSEntityDescription entityForName:@"Drawer"
inManagedObjectContext:managedObjectContext]];
[fetchRequest setSortDescriptors:sortDescriptors];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:@"specialType = nil"]];

self.fetchController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext
sectionNameKeyPath:nil
cacheName:@"drawersOrganiserCache"];

self.fetchController.delegate = self;

NSError *error;
BOOL success = [self.fetchController performFetch:&error];
if (!success)
{
// Handle error
}
}


Then set up the table to display the results from the NSFetchedResultsController in the usual way (this is not a full Core Data tutorial!).

The first part of our reordering magic comes in the tableView: moveRowAtIndexPath: toIndexPath: method. This method is called by the table view when a user has done some reordering.


- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{
self.inReorderingOperation = YES;

NSMutableArray *array = [[self.fetchController fetchedObjects] mutableCopy];
id objectToMove = [[array objectAtIndex:fromIndexPath.row] retain];
[array removeObjectAtIndex:fromIndexPath.row];
[array insertObject:objectToMove atIndex:toIndexPath.row];
[objectToMove release];


for (int i=0; i<[array count]; i++)
{
[(NSManagedObject *)[array objectAtIndex:i] setValue:[NSNumber numberWithInt:i] forKey:@"sortOrder"];
}
[array release];

self.inReorderingOperation = NO;

NSError *error;
BOOL success = [self.fetchController performFetch:&error];
if (!success)
{
// Handle error
}

success = [[self managedObjectContext] save:&error];
if (!success)
{
// Handle error
}

}


So, what's going on here then? The table view sends us two index paths: where the row was moved from, and where the row was moved to. At this moment in time, the UI for the table has been updated (the table view took care of that itself), and the model has not been touched. So the row we want is currently at fromIndexPath in the model.


The first thing to do is set a property to note that you're within a reordering operation. I'll explain this a little more later, but it prevents a loop where the model updates the view and vice versa.


Next, we make a mutable copy of the array of fetched objects. Then, we isolate the object we want, remove it from that array, and add it back in the new place (based on the new index path). Pretty simple, actually.


Next, we need to update the sort ordering properties of the objects. We have an array of them in the correct order, so I just loop through them all in turn, setting each sort order incrementally. If your table is very large, you could optimise this by only updating the sort orderings for rows between the two index paths, as the rest won't have changed. My table was small enough I didn't need to worry about that.


Finally, set the reordering flag back to false, since you've made your updates. Then perform first a fetch then a save. I'm not entirely sure why the fetch needs to be performed first, but it fixed a ton of crazy bugs when I did it!


The last piece of the puzzle: handling your NSFetchedResultsController delegate methods.


- (void)controller:(NSFetchedResultsController*)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath*)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath*)newIndexPath
{
switch (type)
{
case NSFetchedResultsChangeMove:
if (!self.inReorderingOperation)
{
// do stuff to respond to model change
}
break;
}
}


That's only a subset of it. The bit we're interested in is NSFetchedResultsChangeMove. This is called when an item in your model has changed where it is in the order. This of course would happen when you update the sort ordering fields of the objects. What we do here is check that reordering flag that we set before. If it's set to true, we do nothing here (as the GUI has already been updated to the new ordering). If it's false, then the change came from elsewhere so we need to handle that change.