UIButton's Title Label Word Wrap with Tail Truncation
Asked Answered
L

8

22

I need to enable word wrapping and tail truncation, at the same time, on a UIButton's titleLabel. Setting numberOfLines to something more than 0 doesn't work, the text stays on one line.

I've already searched around and haven't found a solution. Any idea?

Laminar answered 11/9, 2011 at 12:52 Comment(5)
#6599331Prismatoid
@Marek how does that solve my problem?Laminar
UILineBreakModeTailTruncation? Truncate text (as needed) from the end of the line. For multiple lines of text, only text on the last line is truncated.Prismatoid
Yeah but multiple lines only work if I add newline characters manually, right? Basically I have one long string and need to show it on multiple lines (word-wrap) and truncate it if it's longer than the label (tail truncation).Laminar
@pt2ph8 No, you don't need to add newline characters manually. The text in the UILabel will automatically wrap onto new lines.Brass
L
5

I solved it the same day I posted this question by putting a UIButton on top of a UILabel with numberOfLines set to 3. I had left this unaccepted to see if someone had a better idea, but apparently there's no other solution.

Laminar answered 11/10, 2011 at 16:57 Comment(2)
On iOS 6 you can do this: label.lineBreakMode = NSLineBreakByWordWrapping | NSLineBreakByTruncatingTail;Codon
@Codon - No, you cannot do that. Look at the type of NSLineBreakMode and you'll see why. (Or the answer above)Glycoprotein
C
44

This is not correct:

lblTemp.lineBreakMode = NSLineBreakByWordWrapping | NSLineBreakByTruncatingTail
lblTemp.numberOfLines = 0;

NSLineBreakMode is defined in NSParagraphStyle.h as:

typedef NS_ENUM(NSInteger, NSLineBreakMode) {       /* What to do with long lines */
    NSLineBreakByWordWrapping = 0,      /* Wrap at word boundaries, default */
    NSLineBreakByCharWrapping,      /* Wrap at character boundaries */
    NSLineBreakByClipping,      /* Simply clip */
    NSLineBreakByTruncatingHead,    /* Truncate at head of line: "...wxyz" */
    NSLineBreakByTruncatingTail,    /* Truncate at tail of line: "abcd..." */
    NSLineBreakByTruncatingMiddle   /* Truncate middle of line:  "ab...yz" */
} NS_ENUM_AVAILABLE_IOS(6_0);

Note that it is an NS_ENUM, not an NS_OPTION, so it is not meant to be used as a mask. For more information see this.

In reality using the | operator on those constants leads to a mask matching NSLineBreakByTruncatingTail:

(NSLineBreakByWordWrapping | NSLineBreakByTruncatingTail) == 4
NSLineBreakByTruncatingTail == 4

As far as I know, truncating the last line in Core Text and also doing word wrapping can not be done with the simple CTFramesetterCreateWithAttributedString & CTFrameDraw APIs, but can be done with line by line layout, which UILabel must be doing.

iOS 6 simplifies this by exposing new drawing APIs in NSStringDrawing.h:

typedef NS_ENUM(NSInteger, NSStringDrawingOptions) {
    NSStringDrawingTruncatesLastVisibleLine = 1 << 5, // Truncates and adds the ellipsis character to the last visible line if the text doesn't fit into the bounds specified. Ignored if NSStringDrawingUsesLineFragmentOrigin is not also set.
    NSStringDrawingUsesLineFragmentOrigin = 1 << 0, // The specified origin is the line fragment origin, not the base line origin
    NSStringDrawingUsesFontLeading = 1 << 1, // Uses the font leading for calculating line heights
    NSStringDrawingUsesDeviceMetrics = 1 << 3, // Uses image glyph bounds instead of typographic bounds
} NS_ENUM_AVAILABLE_IOS(6_0);

@interface NSAttributedString (NSExtendedStringDrawing)
- (void)drawWithRect:(CGRect)rect options:(NSStringDrawingOptions)options context:(NSStringDrawingContext *)context NS_AVAILABLE_IOS(6_0);
- (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options context:(NSStringDrawingContext *)context NS_AVAILABLE_IOS(6_0);
@end

So if you are using UILabel, you want your NSAttributedString's NSParagraphStyle or the lineBreakMode on the label itself to be set to :

NSLineBreakByTruncatingTail

And the numberOfLines property on the label must be set to 0.

From the UILabel headers on numberOfLines:

// if the height of the text reaches the # of lines or the height of the view is less than the # of lines allowed, the text will be
// truncated using the line break mode.

From the UILabel documentation:

This property controls the maximum number of lines to use in order to fit the label’s text into its bounding rectangle. The default value for this property is 1. To remove any maximum limit, and use as many lines as needed, set the value of this property to 0.
If you constrain your text using this property, any text that does not fit within the maximum number of lines and inside the bounding rectangle of the label is truncated using the appropriate line break mode.

The only problem that arises with this somewhat obscure feature of UILabel is that you can not get the size before drawing (which is a necessity for some UITableView + UITableViewCell dynamic layouts) without resorting to modifying the NSAttributedString's NSParagraphStyle on the fly.

As of iOS 6.1.4, calling -boundingRectWithSize:options:context with a NSAttributedString that has a NSLineBreakByTruncatingTail line break mode (for UILabel), returns an incorrect single line height even if the following options are passed in:

(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine)

(Please note that NSStringDrawingUsesLineFragmentOrigin is a necessity for multi line strings.)

What is worse is that UILabel's lineBreakMode does not override the NSAttributedStrings paragraph style, so you have to modify your attributed string's paragraph style for your sizing calculation, and later for passing it to the UILabel so it can draw it.

That is, NSLineBreakByWordWrapping for -boundingRectWithSize:options:context and NSLineBreakByTruncatingTail for the UILabel (so it can, use NSStringDrawingTruncatesLastVisibleLine internally, or whatever it does to clip the last line)

The only alternative if you do not want to mutate your string's paragraph style more than once much would be to do a simple UIView subclass that overrides -drawRect: (with the appropriate contentMode set to redraw as well), and uses iOS 6's new drawing API:

- (void)drawWithRect:(CGRect)rect options:(NSStringDrawingOptions)options context:(NSStringDrawingContext *)context NS_AVAILABLE_IOS(6_0);

Remembering to use NSLineBreakByWordWrapping and passing in (NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine) as the options.

Finally, before iOS 6, if you wanted to do word wrapping + tail truncation for an attributed string you would have to do line by line layout yourself with Core Text.

Charles answered 31/5, 2013 at 1:9 Comment(1)
This is very helpful, however, I believe your claim that "And the numberOfLines property on the label must be set to 0" is incorrect. Both the documentation you quote above, and my own tests on iOS 7.0.3, indicate that you can set the numberOfLines to (say) 3, and the line break mode to NSLineBreakByTruncatingTail, and you will get (at most) that many lines plus tail truncation.Bluefarb
L
5

I solved it the same day I posted this question by putting a UIButton on top of a UILabel with numberOfLines set to 3. I had left this unaccepted to see if someone had a better idea, but apparently there's no other solution.

Laminar answered 11/10, 2011 at 16:57 Comment(2)
On iOS 6 you can do this: label.lineBreakMode = NSLineBreakByWordWrapping | NSLineBreakByTruncatingTail;Codon
@Codon - No, you cannot do that. Look at the type of NSLineBreakMode and you'll see why. (Or the answer above)Glycoprotein
S
5
[self.costomButton.titleLabel setTextAlignment:UITextAlignmentLeft];
[self.costomButton.titleLabel setNumberOfLines:3];

Be sure you should set Alignment first ps: this only work when the system version is bigger than 5.0

Sectarian answered 9/6, 2012 at 5:24 Comment(1)
Set AlignmentLeft worked for me. Thank man.Agamogenesis
S
0

Try setting the numberOfLines more than 2, and set height also accordingly.

    m_button = [UIButton buttonWithType:UIButtonTypeCustom];
[m_button setFrame:CGRectMake(isLandscape?20:10, 40, isLandscape?300:250, 40)];
m_button.titleLabel.font  = [UIFont fontWithName:@"HelveticaNeue" size:17];
[m_btnDiscoverPoint setTitle:@"Title" forState:UIControlStateNormal];
CGRect buttonFrame = [m_button frame];

if ([m_button.titleLabel.text length]>0) {
    CGSize suggestedSize = [m_button.titleLabel.text sizeWithFont:[UIFont fontWithName:@"HelveticaNeue" size:17] constrainedToSize:CGSizeMake(FLT_MAX,m_button.frame.size.height) lineBreakMode:UILineBreakModeWordWrap];

    if (suggestedSize.width >= self.view.frame.size.width) {
        suggestedSize.width = self.view.frame.size.width-10;
        suggestedSize.height=suggestedSize.height+20;
        m_button.titleLabel.numberOfLines=2;
    }
    else{
        m_button.titleLabel.numberOfLines=1;
    }

    buttonFrame.size.width = suggestedSize.width;

    [m_button setFrame:buttonFrame];
}
[m_button setBackgroundColor:[UIColor clearColor]];
[m_button addTarget:self action:@selector(btnClickAction) forControlEvents:UIControlEventTouchUpInside];
Stubborn answered 8/6, 2012 at 6:43 Comment(0)
M
0
button.titleLabel.numberOfLines = 2;
button.titleLabel.lineBreakMode = UILineBreakModeWordWrap;
UIFont * theFont = [UIFont systemFontOfSize: 14]; // you set
CGSize textSize = [titleStr sizeWithAttributes:@{NSFontAttributeName: theFont}];
CGFloat theWidth = kScreenWidth-otherWidthYouSet;// I thought the button's frame is content driving ,and is limited 
CGFloat ratio = theWidth*heightYouSet/((textSize.width+4)*(textSize.height+6));// 4 , 6 , is made by experience . I think the textSize is taken one line text default by the system 
NSUInteger validNum = ratio * titleStr.length;

if(ratio<1){
    [button setTitle: [[titleStr substringToIndex: validNum] stringByAppendingString: @"..."] state: yourState];

}
else{
    [button setTitle: titleStr state: yourState];
}
Middelburg answered 30/11, 2017 at 5:15 Comment(0)
C
0

Swift 5:

You can have multiple lines along with tail truncation by simply setting "numberOfLines = 2" and "lineBreakMode" to "truncate tail" to your UILabel.

enter image description here

Cornish answered 19/3, 2020 at 11:30 Comment(0)
R
-2

All UI properties are deprecated in iOS Use NS abbreviations instead of UI. As shown example here - NSLineBreakByTruncatingMiddle

Retaretable answered 17/11, 2013 at 4:36 Comment(0)
R
-3

You can specify more than one lineBreakMode on a label by using the bitwise OR operator.

For example, the following code would wrap the text of the label, and would add the ellipsis on the tail end of the text when it expanded beyond the size of the label's frame height.

lblTemp.lineBreakMode = UILineBreakModeWordWrap | UILineBreakModeTailTruncation;
lblTemp.numberOfLines = 0;

UPDATE: this is not correct. It appears to work because UILineBreakModeWordWrap is 0 in the enum. See comments below.

Ridgeway answered 11/10, 2011 at 15:1 Comment(4)
This solution may not have solved the original problem, but my problem sounded like this problem and this solution solved my problem...confused yet? anyway, good one!Sprat
Wow, who knew that this supported the bitwise OR operator. I think IB does nothing to help by only allowing you to select a single option.Croon
This totally does not work. Just look at the enum definition to see why. However it seems to work because UILineBreakModeWordWrap is 0 so what you're actually doing is the same as lblTemp.lineBreakMode = UILineBreakModeTailTruncation. This is actually valid (as per the comment in the header) for multiline labels.Glycoprotein
@Glycoprotein You're quite right. Ugh. It's horrible when really wrong answers like this get upvoted so highly.Brass

© 2022 - 2024 — McMap. All rights reserved.