Friday, April 16, 2010

Using Subviews and Table Views in the IPhone

Coming from C# and ASP.Net I've always made UI's easier by taking big complex UI's and breaking them down into manageable, often reusable, chunks. You have some tab on your app that you suddenly want to turn into a dialog? Sure, np: Drag, Drop, Done. You want to add another custom data field editor somewhere? Sure, np: Drag, Drop, Done. Wait, what? you want to have 50 custom data field editors on the same page!? I'll have to go and manually rename the fields for each one to make sure they don't cause conflicts, wow, thats going to be horible to debug... or not: Drag, Drop, Done.

This pattern has worked very well for me in the past. Yes you spend a bit of extra time up front, but I've found you always get it back when you have to make changes, or do maintence.

Enter Xcode. My first week's expirence has shown me that "Drag, Drop, Done" and XCode don't mix. Doesn't mean it's not doable, just takes a bit more more effort, and we can't do it all in one step from the Interface Builder.

Step 1 - Divide and Conquer
This step is common regardless of what language or platform you're aiming for. Take your UI mockups (you do have mockups, right?) and group the controls. As a general rule of thumb group items to minimize the cost of adding things later. it usually works out as group first by common purpose, then by proximity, but there may be exceptions. Lets suppose we have a dialog that lets the user select a few specific files and perform an action on them. We can group the controls pertaining to a single file, and then drop a control for each instance of files we need to have in our dialog. We could also have grouped the controls by function, all the buttons, all the labels, etc, but that wouldn't have helped us because we'd still need to edit multiple places to add an extra control later or do maintence.

Step 2 - Create the Xib File and Controller for our custom controller
In the project, add a new file. UI Controller (with Xib). Use the interface builder to make the control look the way we want it. Edit the controller files to give the control specific behavior like we would normally.

Step 3 - Adding Placeholders for our custom control
In Xcode open the interface (.h file) and implementatio (.m file) for the controller for the window/view that will be holding our custom control. We need to manually add a field to reference our custom control's placeholder.

.h file
//Add our private fields
MyCustomControl* customControllController;
IBOutlet UIView* CustomControlPlaceHolder;
//Add public properties
@property UIView* CustomControlPlaceHolder;

.m file
@synthesize CustomControlPlaceHolder;

Now open the xib file for the window/view that will be holding our custom control in the Interface Builder. From the library window, drag a new View over and adjust size to fit. This blank view is a placeholder that will get replaced at run time with our custom control. Right click on the controller/File's Owner icon and create a link from the placeholder field we created in the controller and the view object we added in the interface builder.

Step 4 - Write code to load our custom control at run time
The code to actually load the control can be done in the AppDelegate (if its a top level control) or can be implemented in the parents controller function, AwakeFromNib, to do it when the parent view is being loaded.
customControlController = [[MyCustomControl alloc] initWithNibName:"MyCustomControl" bundle:nil]
[ParentViewControl.CustomControlPlaceHolder addSubView:customControlController.view];
make sure to release the custom control when the app exits to prevent memory leaks
[customControlController release]

You may be tempted to use autorelease here, but I'd recommend avoiding it. We're creating our view inside an event, and events maintain their own autorelease pools. If you autorelease your view, you may encounter weird errors later on.

Custom Table Views
For tables the process is the same, except that we need more code to handle the table specific parts, and the code for loading the nib files is slightly different. Also instead of dragging a View to our custom control, we'll want to drag a table cell view to our custom control and you should explicitly give it an identifier in the inspector.
Here are the two function you _must_ implement in order to get basic tables working. The names are specific and cant be changed.
//this is where we load our cell's view
- (UITableViewCell*) tableView:(UITableView*)tableView cellForRowAtIndexPath:NSIndexPath*) path
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@"MyCustomTableCell"];
if (cell == nil)
MyCustomTableCellController* newCell = [MyCustomTableCellController alloc];
[[NSBundle mainBundle] loadNibNamed:"MyCustomTableCell" owner:newCell options:nil];
//Figure out how to release this later, can't use autorelease
cell = newCell.customCellPlaceHolder;
return cell;

//return the number of rows to display
- (NSInteger) tableView:(UITableView*) tableView numberOfRowsInSection:(NSInteger) section
{return 42;}

You will probably find you'll want more control. Check out UITableViewDataSource Protocol and
UITableViewDelegate Protocol for a list of optional functions you can implement to have more control over the table

Ok so it's not done, but it works! Yay.

What's Next?
Next I have to figure out how to load the specific data inside my table cells. I'm thinking I'll look for a function that gets called when a cell is created/dequeued similar to how AwakeFromNib is called when a control is loaded and use that to switch the display to the model indicated by the NSIndexPath... Another thing I have to figure out is how to merge the table with my actual data so that I can do that :)

Viewing your control at design time
Unfortunetly I haven't found an easy way to "view" our control at design time. If you _need_ this functionality try looking into making a custom Interface Builder Plugin, this is beyond my needs though so I won't be covering this anytime soon.


No comments:

Post a Comment