Cross-Fade Back Buttons

Maybe it’s just me, but there’s a quirk in the iPhone’s navigation controller infrastructure that just drives me batty: If you set the leftBarButtonItem of a view controller’s navigationItem while the navigation bar is displaying a “back” button, the back button will disappear without animation, producing a nasty “pop” effect. Today, I present a workaround.

Custom Views

We’re attempting to work around the lack of a proper cross-fade between the system “back” buttons and user-defined left buttons. One way to do this is to never use the system “back” buttons in the first place, and to use custom buttons in their stead. The biggest problem with this plan is the unusual chevron shape of the back button, which precludes the use of a conventional UIBarButtonItem.

Fortunately, the UIBarButtonItem class provides the initWithCustomView: initializer. This method lets you put whatever you like on a navigation bar. It turns out that if you supply a UIButton as the custom view, most of the subtler features of the standard navigation bar (e.g. buttons respond to all taps on their “side” of the bar, not just to those in their rendered area) will work properly and automatically.

Creating a Button

To create a UIButton that mimics the appearance of a system back button, we’ll need a few things:

  • Stretchable artwork
  • A lot of experimentation

Taking the experimentation first, I determined (based in no small part on this article) that navigation bar back buttons:

  • Are 30px high
  • Have a 6px border above their text
  • Have an 8px border to the right of their text
  • Have a 12px border to the left of their text
  • Display a white 12-pt bold system font
  • Have a dark gray text shadow w/offset (0, -1)

Based upon this, I added the following method to a “NavButton” category of UIButton:

- (UIButton*)configureForBackButtonWithTitle:(NSString*)title target:(id)target action:(SEL)action
{
	// Experimentally determined
	CGFloat padTRL[3] = {6, 8, 12};

	// Text must be put in its own UIView, s.t. it can be positioned to mimic system buttons
	UILabel* label = [[UILabel alloc] init];
	label.backgroundColor = [UIColor clearColor];
	label.font = [UIFont boldSystemFontOfSize:12];
	label.textColor = [UIColor whiteColor];
	label.shadowColor = [UIColor darkGrayColor];
	label.shadowOffset = CGSizeMake(0, -1);
	label.text = title;
	[label sizeToFit];

	// The underlying art files must be added to the project
	UIImage* norm = [[UIImage imageNamed:@"back_norm.png"] stretchableImageWithLeftCapWidth:13 topCapHeight:0];
	UIImage* click = [[UIImage imageNamed:@"back_click.png"] stretchableImageWithLeftCapWidth:13 topCapHeight:0];
	[self setBackgroundImage:norm forState:UIControlStateNormal];
	[self setBackgroundImage:click forState:UIControlStateHighlighted];
	[self addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];

	// Calculate dimensions
	CGSize labelSize = label.frame.size;
	CGFloat controlWidth = labelSize.width+padTRL[1]+padTRL[2];
	controlWidth = controlWidth>=norm.size.width?controlWidth:norm.size.width;

	// Assemble and size the views
	self.frame = CGRectMake(0, 0, controlWidth, 30);
	[self addSubview:label];
	label.frame = CGRectMake(padTRL[2], padTRL[0], labelSize.width, labelSize.height);

	// Clean up
	[label release];
	return self;
}

Artwork

normalclickedThis is the artwork I pulled together, from screenshots and Photoshop. Please note that these images are designed to work with a particular navigation bar background image; if you're using a different hue (or if AAPL changes the system art) the images will need to be updated. Something with alpha would be more elegant, but that (a.) is more fiddly work and (b.) might well look worse.

Usage

To make all this work, you need some code to create the final UIBarButtonItem, and an action method to manipulate the navigation controller. I like to make both members of a view controller class extension, like this:

@interface SomeViewController ()

@property (nonatomic, retain, readonly) UIBarButtonItem* backButton;
// ::other properties::

- (void)dismiss;
// ::other methods::

@end

@implementation SomeViewController

- (UIBarButtonItem*)backButton
{
	// backbutton is declared as a member of SomeViewController,
	// released properly in dealloc, etc.
	if (!backButton)
	{
		NSArray* controllers = [self.navigationController viewControllers];
		NSUInteger count = [controllers count];
		if (count >= 2)
		{
			UINavigationController* controller = [controllers objectAtIndex:count-2];
			NSString* title = [[[controller navigationItem] backBarButtonItem] title];
			if (!title) title = [controller title];

			backButton = [[UIBarButtonItem alloc] initWithCustomView:[[UIButton buttonWithType:UIButtonTypeCustom] configureForBackButtonWithTitle:title target:self action:@selector(dismiss)]];
		}
	}
	return backButton;
}

// ::other stuff::

- (void)dismiss
{
	[self.navigationController popViewControllerAnimated:YES];
}


@end

Pretty straightforward; the only cutesy stuff involves the logic to determine the title that should be displayed on the button. To invoke this code, you just need a statement like this:

self.navigationItem.leftBarButtonItem = self.backButton;

in, for instance, viewDidLoad.

Disclaimers

There may well be a simpler way to work around this problem, but I couldn't find a decent one. (On the other hand, I presently know little about Core Animation, and it seems likely that something useful might be hiding there.) Furthermore, this is only a small problem (it doesn't even happen when you clear the left button - just when you set it) and one that most people would probably wisely overlook.

Source Code

You can download the implementation and header files, as well as the artwork for the normal and highlighted versions of the button's background. Feel free to use them as you like (technically: BSD 3-clause licensing).

Share and Enjoy:
  • Twitter
  • Facebook
  • Digg
  • Reddit
  • HackerNews
  • del.icio.us
  • Google Bookmarks
  • Slashdot
This entry was posted in iPhone. Bookmark the permalink.