UIButton's Title Label Word Wrap with Tail Truncation

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.


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.


[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

Tags:

Ios

Uilabel