Thursday, July 9, 2009

In Search of a Better Way: Editable Detail Views

Some of the ugliest iPhone code I've written to date - perhaps most of the ugly code I've written to date - has been in table view controllers acting as detail editing panes. Normally, when you use a table view you use it to present a list of data to the user from an array or fetch request, and for doing that, the table view architecture is beautiful. But, when you want to use it to present - and allow the user to edit - properties of a single object, things get a bit gnarlier.

Sure, you can use Interface Builder to build your editing panes. But, most people take their cues from Apple about how to do things, and detail editing views are usually implemented as tables. Take a look at the Contacts app, for example:



It uses table views. So does the Settings application. Let's face it, most of us are going to want to use the same approach.

But, it's hard to write good clean code to handle these types of detail editing views. Because the editing of any particularly property could be handled by a different controller class, it's very hard to write elegant code to implement these. You end up with a lot of similar yet not-easy-to-refactor code.

The most obvious way to write these controllers is to use enums to define your table view's sections, and the rows within each section, sort of like this:

typedef enum  
{
ProjectTableSectionNameSection,
ProjectTableSectionTasksAndExpensesSection,
ProjectTableSectionPrimaryContactSection,
ProjectTableSectionDateSection,
ProjectTableSectionBudgetSection,
ProjectTableSectionLocationSection,
ProjectTableSectionReportSection,
ProjectTableSectionCompleteButtonSection,
ProjectTableSectionDeleteButtonSection,

ProjectTableSectionNumberOfSections
}
ProjectTableSection;

typedef enum
{
ExpenseEditSectionName,
ExpenseEditSectionGeneral,
ExpenseEditSectionDate,
ExpenseEditSectionDeleteButton,

ExpenseEditSectionCount
}
ExpenseEditSection;
...

Then, in the implementation of your class, you write switch statements to handle the logic for each section and row combination. The advantage of this approach is that you can rearrange rows and sections just by changing the enumerations. Because each value in the enum is one higher than the one before, you just change the order of the constants, and the rows or sections change order in the actual table.

But…

If you have an object with many properties, divided up into multiple sections, you end up with nasty, hard-to-maintain code doing that. Sure, you don't have to rearrange or change large chunks of code to rearrange the visual appearance, but it's still hard to find the code you're looking for when you go to make changes or fix bugs and it's still mingling the view and controller parts of MVC together in an uncomfortable living situation. You're violating MVC, and the design of the table view controller is actually kind of encouraging you to do so.

To give you an example, here is an excerpt from one of the first (and utterly horrible) complex table-based editing views I wrote using this technique:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
switch ([indexPath section])
{
case ProjectTableSectionNameSection:
{
TextFieldEditingViewController *controller = [[TextFieldEditingViewController alloc] initWithStyle:UITableViewStyleGrouped];
controller.fieldNames = [NSArray arrayWithObject:NSLocalizedString(@"Project Name", @"Project Name")];
controller.fieldKeys = [NSArray arrayWithObject:@"name"];
controller.fieldValues = [NSArray arrayWithObject:project.name];
controller.shouldClearOnEditing = [project.name isEqualToString:NSLocalizedString(@"Untitled Project", @"Untitled Project - name given to new projects")];

controller.delegate = self;
[self.navigationController pushViewController:controller animated:YES];
[controller release];
break;
}

case ProjectTableSectionPrimaryContactSection:
{
if ([indexPath row] == [project.contacts count])
{
ABPeoplePickerNavigationController *peoplePickerNavigationController = [[ABPeoplePickerNavigationController alloc] init];
peoplePickerNavigationController.peoplePickerDelegate = self;

[self presentModalViewController:peoplePickerNavigationController animated:YES];
[peoplePickerNavigationController release];
}

else
{
ABPersonViewController *controller = [[ABPersonViewController alloc] init];
controller.personViewDelegate = self;
controller.allowsEditing = YES;
controller.addressBook = project.addressBook;
ABRecordRef theRef = [project recordRefForContactAtIndex:[indexPath row]];
controller.displayedPerson = theRef;
[self.navigationController pushViewController:controller animated:YES];
[controller release];

}

break;
}

case ProjectTableSectionReportSection:
{
ReportViewController *controller = [[ReportViewController alloc] initWithNibName:@"ReportView" bundle:nil];
controller.reportHTML = [project projectReportAsHTML];
controller.title = NSLocalizedString(@"Project Report", @"Project Report");
[self.navigationController pushViewController:controller animated:YES];
[controller release];
break;
}

case ProjectTableSectionDateSection:
{
dateBeingEdited = ([indexPath row] == 0) ? project.startDate : project.expectedCompletionDate;
DateViewController *controller = [[DateViewController alloc] init];
controller.delegate = self;
controller.date = dateBeingEdited;
controller.title = ([indexPath row] == 0) ? NSLocalizedString(@"Start Date", @"Start Date") : NSLocalizedString(@"Exp. Completion", @"Expected Completion Date (abbreviated to fit in title bar)");
[self.navigationController pushViewController:controller animated:YES];
[controller release];
break;
}

case ProjectTableSectionBudgetSection:
{
TextFieldEditingViewController *controller = [[TextFieldEditingViewController alloc] initWithStyle:UITableViewStyleGrouped];
controller.fieldNames = [NSArray arrayWithObject:NSLocalizedString(@"Budget", @"Project Budget")];
controller.fieldKeys = [NSArray arrayWithObject:@"budget"];
controller.fieldValues = [NSArray arrayWithObject:[project.budget stringValue]];
controller.shouldClearOnEditing = ([project.budget doubleValue] == 0.0);
[controller setKeyboardType:UIKeyboardTypeNumberPad forIndex:0];

controller.delegate = self;
[self.navigationController pushViewController:controller animated:YES];
[controller release];
break;
}

case ProjectTableSectionTasksAndExpensesSection:
{
switch ([indexPath row])
{
case 0: // Tasks
{
TaskCategoryViewController *controller = [[TaskCategoryViewController alloc] initWithNibName:@"TaskCategoryView" bundle:nil];
controller.project = project;
[self.navigationController pushViewController:controller animated:YES];
[controller release];
break;
}

case 1: // Expenses
{
ExpenseListViewController *controller = [[ExpenseListViewController alloc] initWithStyle:UITableViewStylePlain];
controller.project = project;
[self.navigationController pushViewController:controller animated:YES];
[controller release];
break;
}

case 2: // Change Requests
{
ChangeRequestCategoryViewController *controller = [[ChangeRequestCategoryViewController alloc] initWithNibName:@"ChangeRequestCategoryView" bundle:nil];
controller.project = project;
[self.navigationController pushViewController:controller animated:YES];
[controller release];
break;
}

case 3: // Budget Metrics
{
BudgetMetricsViewController *controller = [[BudgetMetricsViewController alloc] initWithStyle:UITableViewStyleGrouped];
controller.project = project;
[self.navigationController pushViewController:controller animated:YES];
[controller release];
break;
}

default:
break;
}

break;
}

case ProjectTableSectionLocationSection:
{
LocationViewController *controller = [[LocationViewController alloc] initWithStyle:UITableViewStyleGrouped];
controller.project = project;
[self.navigationController pushViewController:controller animated:YES];
[controller release];
break;
}

case ProjectTableSectionDeleteButtonSection:
break;
default:
break;
}

[tableView deselectRowAtIndexPath:indexPath animated:YES];
}


Pretty horrid, huh? Yeah. Don't do that, kids. And what's worse is, all that code was just to handle the user interaction from maybe ten rows of data divided into a handful of sections. And that's just one method in the controller class. There are longer ones, actually. It's ugly, unclean code any way you look at it.

So, ever since I wrote that monstrosity above, I have been on the lookout for ways to make the process of using table views to display and edit properties from a single data object using a table more manageable. Using generic controller classes like the ones I wrote a while back helped some, but not enough and doesn't fix the breakdown of the wall between the C and V of MVC. Unfortunately, I got busy, and I put the quest for a better solution on a back burner.

However, one of the applications we wrote for More iPhone 3 Development (on Core Data) uses one of these detail editing panes, and I really wanted to find something more elegant before I committed the code to print for the world to see. So, I dove back into this problem recently and came up with something I'm actually happy with, though it still needs some refinement. It's a a proof-of-concept stage now, but it's a very promising proof of concept.

Instead of writing a custom controller class for every detail editing view I need, I now can just create an instance of a generic controller class designed for these types of detail editing views. I pass the location of a property list file to the init method of that controller, and that property list defines the structure and appearance of the table-based detail editing view. It supports sections and different types of editors. You can have the user edit an attribute in a text field, or you can present a drop down list without writing any code, and you can add additional editors just by subclassing an existing class. Just assemble a property list using Xcode's built-in property list editor and pass that property list into the generic class.

Want to rearrange rows or sections? Just drag the corresponding entries in the property list to their new location. Want to add a new section or a new row within a section? Just add a new entry into the property list. Want to delete a row or section? Just delete the entry from the property list.

Here's an example from my proof-of-concept application. I've defined a property list like this:


Click for larger version


If you look at the property list, you see that there are two sections defined, and a total of three rows. The resultant application looks like this:



You see? Two sections, three rows, no code. I just instantiate the generic controller with the path to the property list:

    NSString *layoutPath = [[NSBundle mainBundle] pathForResource:@"HeroLayout" ofType:@"plist"];
ManagedObjectDetailEditor *controller = [[ManagedObjectDetailEditor alloc] initWithLayoutFile:layoutPath];
controller.managedObject = newManagedObject;
[self.navigationController pushViewController:controller animated:YES];
[controller release];

You might have noticed that the last dictionary in the property list (representing the sex of the superhero) has an additional key value called arguments. This gives the flexibility to pass additional data to the controller class, so you can do things like present a list of values that the user can choose from. In this case, we let them choose the sex from a list that includes Male and Female rather than making them type in free-form text.



This code is still in its infancy with many datatype editors left to be developed, but I think there's a lot of potential here for saving developers time. I'm even thinking about developing a little tool to let you visually design the table based on the Core Data data model - but that would be quite a ways down the line. Even without that and just crafting property lists rather than custom classes, there's a huge time savings.

The source code will be available as part of the More iPhone 3 Development source code archive, and one section of the book will include a tutorial on how to create a property list to define a detail editing view layout.



17 comments:

Michael said...

very nice and a great example to help sell the new book.

I too have written lots of very ugly code but unlike you I can't face going back over it....roll on the new book.

Akuma said...

Oh thank you so much for sharing the ugly code. i felt so bad when i wrote something similar and now i know i am not alone.

Did you consider following the great Joe Hewitt and releasing this as open source on github?

I know you want to sell books, but we all gonna buy it anyway ^^

Patrick Alessi said...

Awesome article, as usual! I had this exact same problem in my (2nd) latest app CNotes. It is an app that lets you write notes about contacts in your iPhone address book. Sorry for the cheesy plug, but it helps explain why I had the same issues...

Anyway, I had the same issues! I wanted to make my app look and feel as much like address book as possible, and this included table based editing. Just as you concluded, it was a pain, and was pretty ugly. I had to write custom code for each different editor type. I though about doing something more generic, but I figured since I'm just 1 dev, I could just code it specifically and reuse that code later if I did another app like CNotes.

Anyway, I am really excited to see what you end up with for the book.

Good luck and thanks for your effort. You are definitly advancing the state of the art of iPhone development with your engineering solution to issues like this.

Patrick Alessi

Jeff LaMarche said...

Akuma:


The source code will be made available on Google code when it's done. It's in too early of a state to publicly release right now, but it will be freely available to everyone once I'm confident that it's done.

Jeff

shingoo said...

Bought you book some months ago andhave been reading your blog even longer. Thanks for all your work... It's really really what I searched for to dive into iPhone development.
I'm really looking forward to your new book.

Thanx a lot.

Adrian Kosmaczewski said...

I think the iPhone OS uses something like this to build the "Settings" screen for custom apps, reading the Settings.plist file bundled with the binary... don't you think? This is *really* cool. Thanks! Cheers

Jeff LaMarche said...

Adrian:

Yes, the Settings app does do something like this. It's where I got the basic idea. I find their method a little cryptic, though, so I'm hoping mine ends up being a little easier to use and explain.

Ole Begemann said...

That's a very good approach, Jeff. I am just wondering if it's a good idea to make such a table controller dependent on Core Data. It might be a good thing to illustrate a Core Data example in the book but I'd rather not have the table controller require a NSManagedObject instance.

I think for many usage types (e.g. application settings) a delegate-based approach or plain simple key-value coding that works with any NSObject instance might be better. I wrote something like this a few months ago for myself and my aim was to provide the functionality of the Settings.bundle in my app so I used the same syntax for my plist config file. I never polished it well enough to publish but I might eventually do so now that I have a blog.

Jeff LaMarche said...

Ole:

I have done the delegate method before, and it works, but it's more code clutter than is necessary when working with Core Data. By writing to the managed object, I don't have to have a delegate, I just save the MO's context, which keeps the code nice and clean.

Perhaps I'll generalize it out at some point, but for my purposes now, I need the best generic editor for Core Data that I can write, so that's what I'm writing. I'll make the source available under a liberal license, though, so if anyone wants to tweak it to work with other objects, it should be easy enough.

serpah said...

As many have already commented, I've also written horrible code (like yours) to implement an editable detail view.

Thanks for posting your idea with the pList; it's awesome. I'm going to try that method once I get the rest of the code working.

Breeno said...

I was just chatting with a compatriot today about this sort of approach for prototyping purposes.

Something akin to the notions presented at the WWDC Prototyping UI session (which was more art/image centric) but a bit more programmatic.

I hadn't followed the socratic breadcrumbs to your conclusions quite yet - so this is a nice find!

Great stuff - thanks very much for sharing as freely as you do.

mannkind said...

Jeff,

Has the code demonstrated in this article been released yet? If the code has been released, how might one find it?

Thanks.

Jerm said...

Did this code actually get released with the book in the end?

donmesserli said...

Just purchased the book and it doesn't look like it made it in.

anastacia said...

thanks for the information on this blog! I find it very interesting and entertaining! hopefully soon have updates that I love your post! I thank you too!
buy viagra
viagra online
generic viagra

katty said...

woww, nice phone. i must to say This blog is really useful, actually i was looking for something different like a guide and i think a reached now in this blog. Honestly after to read this blog, i have too much knowledge than before. I usually prove new thing and i wait the result. Actually i suggested to my husband to buy viagra, it was really amazing!!!

h4ns said...

I was very encouraged to find this site. I wanted to thank you for this special read. I definitely savored every little bit of it and I have you bookmarked to check out new stuff you post.

AC Milan vs Lazio Live Streaming
West Bromwich Albion vs Wigan Athletic Live Streaming
Manchester United vs Aston Villa Live Streaming
Sunderland vs Chelsea Live Streaming
Arsenal vs Everton Live Streaming
Augsburg vs Bochum Live Streaming
Racing Santander vs Valencia Live Streaming
Frosinone vs Atalanta Live Streaming
AC Milan vs Lazio Live Streaming
West Bromwich Albion vs Wigan Athletic Live Streaming
Manchester United vs Aston Villa Live Streaming
Sunderland vs Chelsea Live Streaming
Arsenal vs Everton Live Streaming
Augsburg vs Bochum Live Streaming
Racing Santander vs Valencia Live Streaming
Frosinone vs Atalanta Live Streaming
Technology News | Hot News Today | Live Stream