Good iPhone Practices
The iPhone SDK has now been around long enough where we can start to pick out good practices in using some of the more “fuzzy” areas. There are two small, but important, practices that can make your life much easier.
Buttons in UITableViewCells
Sometimes, you will want to have a button or some sort of control in a UITableViewCell subclass. You could have many rows, and each one has a button, and that button should have a different result depending on what row it is in.
In sticking with the MVC paradigm, how do you accomplish this? You have to be able to set the target-action pair for these buttons and you also have to decide which row’s button was pressed.
Well, you don’t even have to look at the UITableViewCell, you are really only concerned with the button. A UIButton (or any UIControl) is a subclass of UIView. Every UIView has an integer instance variable called tag. You can use this tag to specify the row the button is currently in.
1. When a UITableViewCell is created (and only when it is created), you add a target-action pair from you UIButton to your UIViewController subclass to call the method you want.
2. When a UITableViewCell’s data is prepared, you set the tag of its button to the row that cell is occupying.
3. When the action message of your UIButton is sent to your UIViewController subclass, you simply grab the tag of the sender to determine what row it is.
Your UITableViewCell subclass interface should look like this:
@interface ButtonCell : UITableViewCell
{
UIButton *button;
}
@property (readonly) UIButton *button;
@end
And your cell retrieval method should look like this:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
ButtonCell *aCell = (ButtonCell *)[tableView
dequeueReusableCellWithIdentifier:@"ButtonCell"];
if(!aCell)
{
aCell = [[[ButtonCell alloc] initWithFrame:CGRectZero
reuseIdentifier:@"ButtonCell"]
autorelease];
[[aCell button] addTarget:self
action:@selector(buttonPressed:)
forControlEvents:UIControlEventTouchUpInside];
}
[[aCell button] setTag:[indexPath row]];
return aCell;
}
And finally, your action message should do this:
- (void)buttonPressed:(id)sender
{
int rowOfButton = [sender tag];
[[internalData objectAtIndex:rowOfButton] performFunStuff];
}
All there is to it.
Instantiating UIViewController subclasses
XIB files are confusing. Chaining UIViewController XIB files and creating instances of them in another XIB file is even more confusing. And quite pointless. There is a much better way to do it.
First, you have to decide if you want to use a XIB file or not. The *only* reason to use a XIB file is if you have a complicated user interface that needs to be set up via Interface Builder. Otherwise, if you have a single view like a UITableView, UIImageView or UIView, simply implement loadView.
Now, regardless of how you are creating the interface, all UIViewController subclasses (save the standard ones, like UITableViewController, UINavigationController and UITabBarController) should be instantiated in code and sent an init message.
ViewControllerSubclass *vcs = [[ViewControllerSubclass alloc] init];
You are probably wondering, “Uhh… if I just send it init, how do I load its view from a XIB?” Good question. You will be overriding the init method of your UIViewController subclass (and overriding the superclasses designated initializer).
- (id)init
{
return [super initWithNibName:@"ViewControllerSubclass" bundle:nil];
}
- (id)initWithNibName:(NSString *)name bundle:(NSBundle *)bundle
{
return [self init];
}
How does this work out in code? Let’s pretend you have two UIViewController subclasses, RootViewController and DetailViewController. They are both intended to be part of a UINavigationController.
In your application delegate, you should be doing this:
- (void)applicationDidFinishLaunching:(UIApplication *)application
{
RootViewController *rootViewController =
[[RootViewController alloc] init];
UINavigationController *navController = [[UINavigationController alloc]
initWithRootViewController:rootViewController];
[window addSubview:[navController view]];
[window makeKeyAndVisible];
}
In RootViewController’s implementation, you can do a little something like this:
- (void)viewDidLoad
{
if(!detailViewController)
detailViewController = [[DetailViewController alloc] init];
}
Therefore, none of your other classes need to know anything about any other UIViewController subclass. They just know if they want an instance of it – with a fully configured user interface – they can just send it init.
Easy enough.
Posted by Joe Conway on March 18th, 2009 under iPhone.
Comments: 7
Comments
Comment from PhoneyDeveloper
Time: March 18, 2009, 11:20 am
initWithNibName is the designated initializer. If you override initWithNibName and initialize by calling alloc/init, initWithNibName will be called. That’s what I do. I do hardcode the nib name though because I believe that in most cases the nib name is an implementation detail of the vc and the calling code doesn’t need to know it.
Comment from Vincent Gable
Time: March 19, 2009, 2:35 pm
Why is the initWithNibName:bundle: message sent to super, and not self, in the UIViewController subclass’s init method?
Comment from Scott
Time: March 29, 2009, 9:59 am
Instantiating UIViewController subclasses
Just what the Doctor ordered. Thanks!
Comment from Vincent Gable
Time: April 1, 2009, 8:37 pm
To clarify, I think sending the initWithNibName:bundle: to super, not self, is dangerous, because if you implement your own initWithNibName:bundle: method, then it will not get called from -init.
Comment from joe
Time: April 2, 2009, 1:23 pm
It is up to the developer of a class to determine what its designated initializer is. In this case, the designated initializer is simply -init. If you need to do extra initialization, you would do it inside -init. If you were strictly adhering to initializer rules, you would override initWithNibName:bundle: to call your own -init (the designated initializer) and ignore the parameters sent to it.
The rule you cannot ignore is that the designated initializer of a class needs to call the superclass’s designated initializer, and that is what this is doing.
Comment from Brendan Duddridge
Time: May 17, 2009, 3:45 pm
What if you have multiple buttons in a UITableViewCell? Think of a ratings system where you have 5 stars on multiple rows. You can’t use the tag for the row number anymore because you need unique tags for each instance of your 5 star buttons.
Comment from joe
Time: May 31, 2009, 9:34 pm
In your example, you would be better served subclassing UIView and creating a RatingView. This RatingView would track touches and decide the number of stars to display inside of it based on the location of that touch. Why? Consider the caveats of the 5 button approach. Each button would send a message to a controller object to say “Hey, I’ve been tapped.” The controller object would then go to each of the 5 star buttons and say “Button four was tapped, so if you are less or equal to four, you need to show your highlighted state. Button five, you show a dimmed state.” That code would be cumbersome (think lots of if statements) for one, but the most important part is that it would not be reusable. In sticking with MVC, controllers have the least reusability of the three. If you wanted to re-implement your rating system in another view or another project, you’d have to add new code to the controller. By keeping your RatingView self-contained, it is not only reusable, but you can safely tag it with the row. The general lesson is that if you have a group of controls that act in concert, they should be composited in one class (if you are familiar with the desktop side of things, consider what NSMatrix does for a group of radio buttons – where the state of each button relies on the state of the group).
In the case of having multiple buttons per row that are not part of a group, the tag only needs to be unique for the action message the button generates. For example, you could have a table view that shows a forum post in each row. For each forum post, there is a button to vote it up and to vote it down. These buttons would have the same tag that corresponds to the row they are in. Both of the buttons invoke a different method (voteUp:, voteDown:), you can safely figure out which row the tapped button was in (by its tag) and which button it was (by the method that gets invoked).
Write a comment