Wednesday, April 22, 2009

Delegation And Redesign

One side effect of programming for the iPhone (or Mac OS X) is a heightened awareness of the delegation pattern. It seems like every class in the framework has delegate information.

When I started my UI redesign work in my iPhone app, I saw a perfect opportunity for implementing my own delegate pattern.

The idea behind delegation is straightforward. Object A holds a reference to Object B. Object A goes about its merry way performing its duties, except that every now and then it says to Object B, "Hey, I need you to handle this, okay?" To everyone else, it looks like Object A did the work. A good example is the UITableview in the iPhone framework. It supports two delegates, one for supplying the table's data, one for handling the table's behavior. The data delegate has methods such as numberOfSectionsInTableView; the behavior delegate has methods such as tableView:didSelectRowAtPath:. The table view doesn't care about anything else those objects might do: It just expects them to do what it needs.

The pattern encourages code reuse (I have a single instance of a delegate that handles common behavior for all of my text fields) and separates business logic from other layers (I have specialized objects that act as my tableview data sources, leveraging a model object in different ways, all without modifying UITableView in any way). It lets you make complex object compositions from subsystems. It makes breakfast for you in the morning. It's great. (As with all general-purpose design patterns, you may need to consider performance and threading issues when putting it into practice.)

In Objective C, it's easy to use a delegate:

id delegate; // set by an outside client
[delegate invokeDelegateMethod]

Even though the compiler doesn't know if delegate implements invokeDelegateMethod, it compiles this code with only a warning. (id is shorthand for "an otherwise untyped object.") Of course, if you try and run this, and delegate does not implement invokeDelegateMethod, you'll get a runtime exception. This may be what you want, but you can also use:

id delegate;
if ([delegate respondsToSelector:@selector(invokeDelegateMethod)]) {
[delegate invokeDelegateMethod];

This makes the method optional. You can be formal and specify a protocol that the object must declare to get yourself some type safety, but it's not strictly necessary. Java has similar "rummage around in an object's belly and see what it contains" tools in its reflection API, so you could easily hook up delegate patterns the same way in that language.

For my redesign, I had several places where I needed to pop up a view that had a UIDatePicker, a Done button, and a Cancel button. The idea being that the user needs to pick a date and tell me when s/he's done. The delegate pattern seemed to be the perfect approach, since I had one class that I needed to reuse throughout the application and have it fit seamlessly into the flow of each area.

In my first pass, I implemented two delegate methods, dateSaved: (NSDate *)date and datePickerViewDismissed.

I made the first one optional: Why should the date choosing view care whether the client actually wants to know about the date that was chosen? I made the second method required (i.e., I don't check to see if the delegate supports the method before calling) because that's the method that triggers the client to release any memory it used for the view. I wanted to make sure that I'd know right away if I forgot to implement that method.

I wired the view into the first use case: A screen that shows one date at a time would let the user jump to any arbitrary day, rather than nexting and previousing a bunch. Everything worked fine, so I started to wire it into the next part of the app, where you set the time for a single task. That's when I had the eye candy idea for my ScheduleButton. With a bit more work, I could make a delegate method for the date picker view that would fire as the date changed, which meant that I could update the button's text as the user spun the wheels.

My new version added a new delegate method, dateChangedWithoutSaving: (NSDate *)date. But what if the user cancels? My architecture didn't support that, so I changed the datePickerViewDismissed method into datePickerViewDismissedWithButton: (DateChooserButton)btn (DateChooserButton being an enum representing the two choices). Now a client could grab a copy of the date, bring up the view, update the ScheduleButton helper object as the date changed, and then reset it to the original date if the user pressed cancel.

No comments:

Post a Comment