Unsolicited Feedback

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.

multiline-uibutton.png


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.


23 Comments

Jesus, you are genious. This works the way it is.
Just put it into your code, and replace UIButton with UFMultiLineButton!

Bravo and BIG Thanks.

Posted by MB on 14 March 2009 @ 12am

YOU ARE THE BOSS….this saved me a boatload of time. Now , if someone could get adjustsFontSizeToFitWidth working for multi-line labels…

THANKS!

Posted by Mary Albanese on 17 March 2009 @ 6pm

Great! But I’m confused – I downloaded the zip and added the code to my project, and replaced my UIButton with this custom subclass in the critical place. When I run, no problems, but I still don’t get word wrapping. And when I set a breakpoint in – (CGRect)titleRectForContentRect:(CGRect)rect, I see that the method is not being invoked at all.

Do I need to invoke the method myself somewhere?

Oh, just while writing this I just realized what I did wrong… but I’ll put it here for others to learn from: I forgot to change the class in IB.

Let me check…

Yes, it works now! Thanks!

Posted by Mark on 9 April 2009 @ 3am

Can you tell me how to make this multiline button highlight when touched? I.E.
show some short of shading or visual indicator that it has been touched? I’ve tried:

[multilineButton setBackgroundImage:buttonImagePressed forState:UIControlStateHighlighted];
multilineButton.adjustsImageWhenHighlighted = YES;
multilineButton.showsTouchWhenHighlighted = YES;

Otherwise your multi-line button works great. Thanks!

Posted by James Testa on 11 May 2009 @ 4am

James,

You have three possibilities:
1) Set the background image for highlighted state and the properties you suggest in Interface Builder;
2) Set the same as 1) but programatically in the awakeFromNib: of UFMultiLineButton;
3) Set the same as 1) but programatically right after creating the UFMultiLineButton instance.

I just checked if these three are working with the sample code I provided. All of them worked as expected. As an example, here’s the code I used to create the button programatically:

UFMultiLineButton *b = [UFMultiLineButton buttonWithType:UIButtonTypeCustom];
[b setFrame:CGRectMake(40, 190, 240, 100)];
[b setBackgroundImage:bgNormal forState:UIControlStateNormal];
[b setBackgroundImage:bgPressed forState:UIControlStateHighlighted];
[self addSubview:b];

And that’s it. If it still ain’t working for you, please, provide more context from where you are setting these properties.

Posted by jpedroso on 11 May 2009 @ 12pm

Here’s my code below. I created a PNG image the size of the CGRect called highLight.png.

UIImage *buttonImagePressed = [UIImage imageNamed:@"highLight.png"];
MultiLineButton *answerOne = [MultiLineButton buttonWithType:UIButtonTypeCustom];
[answerOne setFrame: CGRectMake(40, 157, 240, 60)];
[answerOne addTarget:self action:@selector(nextQuestion:) forControlEvents:UIControlEventTouchUpInside];
answerOne.adjustsImageWhenHighlighted = YES;
answerOne.showsTouchWhenHighlighted = YES;
[answerOne setShowsTouchWhenHighlighted:YES];
[answerOne setImage:buttonImagePressed forState:UIControlStateHighlighted];
self.answer1 = answerOne;
[self.view addSubview:self.answer1];

Posted by James Testa on 12 May 2009 @ 11am

James,
You are using setImage:forState:. I guess what you really want is setBackgroundImage:forState:. Other than that, your code should work just fine.

Posted by jpedroso on 12 May 2009 @ 5pm

Got it to work. I had commented out

[titleLabel setBackgroundColor:[UIColor clearColor]];

in MultiButton. Uncommenting it fixed the problem.
Thanks for your help!

James

Posted by James Testa on 12 May 2009 @ 9pm

I used your “access the private label” method flawlessly.
Thanks!
I wanted buttons whose labels font size adjusted to the title length. In my controller’s “viewDidLoad” method, I just invoke the following method on each button I want to change:

- (void) rewireButtonLabel: (UIButton *) button {
for (UIView *s in [button subviews])
if ([s isKindOfClass:[UILabel class]]) {
[(UILabel *)s setAdjustsFontSizeToFitWidth:YES];
[(UILabel *)s setMinimumFontSize: 10];
}
}

MUCH easier than creating a new UIButton subclass, or using a label with a transparent button over it.

Posted by Scott on 22 May 2009 @ 7pm

I have 1 problem in this…
when i run this application on iphone os 3.0 than the multilineButton text not display. only image display, can u please tell me what is the reason

Posted by Alok on 17 August 2009 @ 9am

Alok,

I just checked the UFMultilineButton on iPhone OS 3.0, with a title and image, and both displayed as expected.

iPhone OS 3.0 introduced a titleLabel property in UIButton, so I’d suggest you to debug the label frames by overriding -drawRect: in UFMultilineButton. You can use the code snipped below.

- (void)drawRect:(CGRect)frame
{
[super drawRect:frame];
NSLog(@”UIButton titleLabel size: %@”, NSStringFromCGRect(self.titleLabel.frame));
NSLog(@”UFMultilineButton titleLabel size: %@”, NSStringFromCGRect([self viewWithTag:TITLE_LABEL_TAG].frame));
}

Expected logs will be something like:
[...] UIButton titleLabel size: {{0, 0}, {0, 0}}
[...] UFMultilineButton titleLabel size: {{0, 0}, {280, 62}}

Posted by jpedroso on 17 August 2009 @ 11am

This works for me, from standard UI.

First i’m calculating size of text used in button, i’m making CGRect based on calculations and im setting this rect as button frame.

Than:

[[button titleLabel] setLineBreakMode: UILineBreakModeWordWrap];
[[button titleLabel] setNumberOfLines:0];

*** Cheers ***

Posted by Sergi on 16 September 2009 @ 2pm

Hi,

Does the BSD License of this code require me to credit you in my large commercial application if I use it? Unfortunately, my boss would not allow that…

Posted by curious on 27 September 2009 @ 2am

Hi,

I just found this is no longer necessary. Just use a normal UIButton and use btn.titleLabel.lineBreakMode=UILineBreakModeWordWrap and btn.titleLabel.numberOfLines=0

Posted by curious on 28 September 2009 @ 3am

Sorry, for off top, i wanna tell one joke) What do you call a day that follows two days of rain? Monday.
___________________________
–/ viafra online New York /–

Posted by idarop on 29 October 2009 @ 8pm

Fresh joke! What happens when a ghost haunts a theater? The actors get stage fright.
___________________________
–/ viafra herbal /–

Posted by gigiken on 29 October 2009 @ 9pm

You have to write all that confusing code… just to add 1 linefeed in a button????

Why isn’t there just a simple setting for such a common thing???

Posted by susan on 21 March 2010 @ 2am

This is overkill for the task. The simple/usual solution is:

button.titleLabel.lineBreakMode = UILineBreakModeWordWrap;

Don’t you agree?

Posted by garconte on 8 November 2010 @ 1pm

THanks Man :)

Posted by theamitom on 5 August 2011 @ 7am

wow… and to think all i typed was UIbutton. lol What a great resource.

Posted by elelbriel on 3 December 2011 @ 6pm

I loved the page! <3

Posted by Bianca on 31 January 2013 @ 5am

Blog na piątkę

Posted by rybak on 28 July 2013 @ 2pm

Hey! Someone in my Myspace group shared this site with us so I came to give it
a look. I’m definitely loving the information. I’m book-marking
and will be tweeting this to my followers! Terrific blog and amazing
design.

Posted by used cars chances on 29 June 2014 @ 9pm

Leave a Comment