How to Set a Multi-line Title in UIButton
I’ve seen lot of people complaining to the fact that UIButton doesn’t support a multi-line title. As of iPhone SDK version 2.2.1, UIButton’s title frame is always an one-liner, no matter how long the text is.
People are complaining and getting confused because UIButton seems to have the interfaces needed to do so cleanly, namely -setLineBreakMode: with UILineBreakModeWordWrap which would affect the title label, and then adjusting -titleRectForContentRect: to manage room to accomodate multi-lines, but happens that both don't do nothing about it, so I'll show how to do it dirtily. Or sort of.
After some time, the best solution I came up with was to add a UILabel as subview to replace the title. I took the long run in order to make this patch work as seamless as possible with existing UIButton API and Interface Builder. I wanted this code to handle both the title and appearance settings that are defined both ways.
Below is the code that should be added into an UIButton subclass. Note that if you don't need/want to subclass you can pull of this exact same code into your button's superview drawing routines, replacing all references to self with the button variable name. Off to what matters.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | #define TITLE_LABEL_TAG 1234 - (CGRect)titleRectForContentRect:(CGRect)rect { // define the desired title inset margins based on the whole rect and its padding UIEdgeInsets padding = [self titleEdgeInsets]; CGRect titleRect = CGRectMake(rect.origin.x + padding.left, rect.origin.x + padding.top, rect.size.width - (padding.right + padding.left), rect.size.height - (padding.bottom + padding].top)); // save the current title view appearance NSString *title = [self currentTitle]; UIColor *titleColor = [self currentTitleColor]; UIColor *titleShadowColor = [self currentTitleShadowColor]; // we only want to add our custom label once; only 1st pass shall return nil UILabel *titleLabel = (UILabel*)[self viewWithTag:TITLE_LABEL_TAG]; if (!titleLabel) { // no custom label found (1st pass), we will be creating & adding it as subview titleLabel = [[UILabel alloc] initWithFrame:titleRect]; [titleLabel setTag:TITLE_LABEL_TAG]; // make it multi-line [titleLabel setNumberOfLines:0]; [titleLabel setLineBreakMode:UILineBreakModeWordWrap]; // title appearance setup; be at will to modify [titleLabel setBackgroundColor:[UIColor clearColor]]; [titleLabel setFont:[self font]]; [titleLabel setShadowOffset:CGSizeMake(0, 1)]; [titleLabel setTextAlignment:UITextAlignmentCenter]; [self addSubview:titleLabel]; [titleLabel release]; } // finally, put our label in original title view's state [titleLabel setText:title]; [titleLabel setTextColor:titleColor]; [titleLabel setShadowColor:titleShadowColor]; // and return empty rect so that the original title view is hidden return CGRectZero; } |
Shortly, what I did there was to save all settings that define the appearance of the original label for a given current state, apply that settings to the custom label, and return an empty frame so that the original title isn't drawn.
As to the decision of putting this code in -titleRectForContentRect:, it is the method which gives me the best way to handle the change of state when the button is pressed. Surprisingly, -drawRect: is only called once.
That's a truck load of code for such basic good, but hey, that's why I set it up here neatly. And you can use the same UIButton API to set the title, title color and title shadow color without being worried about the extra UILabel.
Here's the final result.

You can download the code below.
Sidenote
An alternate way, is to hack the title's view UILabel. By inspecting UIButton.h, one sees the private UILabel *_titleView, but even subclassing UIButton, there's no way to access directly to _titleView.
Though, rembember: UIButton is a UIView subclass—it has subviews and it's possible to iterate over them. Looking carefully into UIButton.h, we can assume there's only one UILabel. I subclassed UIButton and in -drawRect: I hacked the following:
1 2 3 4 5 6 7 8 9 10 | - (void)drawRect:(CGRect)rect { // looking for _titleView UILabel (see UIButton.h) for (UIView *s in [self subviews]) if ([s isKindOfClass:[UILabel class]]) [(UILabel *)s setNumberOfLines:0]; [self setLineBreakMode:UILineBreakModeWordWrap]; [self setTextAlignment:UITextAlignmentCenter]; // ... } |
Note to self: Sadly, this code doesn't work in -awakeFromNib nor -initWithFrame: — Oddly, the latter is never called when the button is created from a NIB. Also, beware this code is messing with UIKit private stuff. Use with advisory. For what it's worth, my experience tells me it doesn't hurt Apple much.
The original title view label is now capable of handling multiple lines and nicely word wraps its content. Only thing left is changing the frame for title. See the code above on -titleRectForContentRect: for how to generate the frame and return it instead of the CGRectZero.
The only problem with this approach—besides messing around private views—is that the title will never be center aligned. Even changing the code that makes the title's label multi-line and center aligned from drawRect: to -titleRectForContentRect: doesn't fix the text alignment problem.
Bottom line: If you can get away with a left aligned multi-line title then you can consider this approach. Otherwise, don't.
17 Comments