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.

Published by jpedroso

Unsolicited Feedback is a blog by Jorge Pedroso, a software developer, conspirator, user interface and user experience designer aspirant focused on Mac OS X and iPhone platforms. Here he expresses, mostly, opinionated ramblings on software development, code, user experience and interface design.

25 replies on “How to Set a Multi-line Title in UIButton”

  1. 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.

  2. 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!

  3. 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!

  4. 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.

  5. 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];

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

  7. Got it to work. I had commented out

    [titleLabel setBackgroundColor:[UIColor clearColor]];

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

    James

  8. 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.

  9. 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

  10. 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}}

  11. 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 ***

  12. 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…

  13. 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

  14. 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???

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

    button.titleLabel.lineBreakMode = UILineBreakModeWordWrap;

    Don’t you agree?

  16. 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.

  17. This “loop beats box” mentality can really drive both how you think about collecting customer feedback in your organization, as well as how you make product decisions. If you set up a genuine, multi-touch closed feedback loop with your customers, your team can break out of silos and improve your product and user experience at a quicker and more productive pace.

Comments are closed.