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.

Thursday, 8 October 2009

Creator Codes and Modern File Browsing

John Gruber recently commented on Snow Leopard's lack of creator codes, or any replacement functionality. For the uninitiated, creator codes were the mechanism by which the classic MacOS handled double clicking on a file: the file was always opened by the application that created it. On other OS's, all JPEG files had to be opened by the same program. On classic MacOS, those that were created in Photoshop would open in Photoshop, and those created in GraphicConverter would open in that by default, and so forth.

MacOS X apps didn't tend to set creator codes by default. Some apps ported from MacOS 9 such as Photoshop would set them, but the majority did not. However, if a creator code was set, the system would honour it and open the correct app… until Snow Leopard. In Snow Leopard, the only way to make a file open with a different app to the default is to set it manually in the Get Info window. Crucially, an application cannot set that while saving, the user has to go and do it. 

I'm not going to comment on the state of play here, but rather on a point Gruber made:
Take a step back and consider that the term creator code itself shows just how different things are today. When the Mac was created, nearly all documents were proprietary binary file formats. The only app that could read MacWrite files was MacWrite, etc. Even when apps could read other apps’ file formats, they typically did so only through an import/export process.
He goes on to say that now we work with many different types of open file format in many apps, so the question "Which app should open this file?" is not always answered with "The app that created it".

Thinking back to when I first switched to Mac, back in 1993 (the System 7 days), I don't think the proportion of open file types was too much lower. We saved graphics as TIFF, vector stuff as PICT, text files as plain text, databases as comma separated. Granted, I had loads of ClarisWorks or Quark files, but they're comparable to all the iWork and inDesign files I have today, which are also in proprietary formats. I don't think that's it.

You may have read John Siracusa's laments about the demise of the spatial Finder. To sum up, a spatial file browser has one window per folder, and opens each folder in a new window. That window appears in the same place as it did last time you opened that folder. To the user, the window (size/position/layout) is the folder. We've now switched to a browser style file manager, where a window is just a helpful device to pick files, and you move them around with impunity, knowing that your settings are not stored anywhere. Think about iTunes and iPhoto: browser based media managers. Even opening an image in Preview presents you with a window with a sidebar for putting multiple images in a single window. Compare to opening one in SimpleText back in the day, where the picture had its own window with nothing else in it.

In the user's terms, it used to be that the window was the document. The two were equivalent. Now, in a trend that started with the World Wide Web and has now moved into the rest of the operating system, a window is merely a container for one or more documents. That's a fundamental change in the user's concept of what a window is.

How does this relate to creator codes? Well, if the user equates a document and its window, then it is completely natural to open a document in the same app. Anything else would violate that mental model. Sure, a file could be manually opened by dragging it onto another app's icon, but that separate action was considered to be a "convert" action. Just opening a document put it exactly as you left it last time, with window position saved, the same editing tools available, and other such things.

With the trend to browser style windows, there's no longer that expectation. Double clicking a file no longer means restore the state of this document so I can work with it. And that's why it's no longer reasonable to say that the best app to open a document is the one that created it.