5

I use a table view to show a list of books, where each cell has a UILabel that shows the book's name and another UILabel the shows the book's author(s)

My question is about the author(s) label. A book can have multiple authors, and I want it to behave as follows:

  • If book has one author ('John Colman') label should be: "John Colman"
  • If book has more than one author ('John Colman', 'Bob Night', 'Michael') label should be: "John Colman +2 authors"

Now the problem is this, I want the label to be truncated before the '+'. So for example, if the first author name is long, lets say 'Benjamin Walter Jackson', I want the label to look like this:

"Benjamin Walter Ja... +2 authors"

The default behaviour of course truncates the label in the end, so it looks like this:

"Benjamin Walter Jackson +2 au..."  

If I use the middle truncate, there's no promise that it will truncate the label in the right place (before the '+')

I'm looking for a way to do it and as efficient as possible, without impacting the scroll performance of the table view.

Stonz2
  • 6,306
  • 4
  • 44
  • 64
Eyal
  • 10,777
  • 18
  • 78
  • 130
  • Hm maybe the easiest way would be to use 2 labels. One for the name and other for the +2.... So if your name doesn't fit it will get ... at the end and if it does, than you resize the first label to the string size and move second label at the end of the first. I see a lot of calculations if you try it differently ;) – AntonijoDev Jul 16 '14 at 14:40

3 Answers3

5

Edit: Generalized the solution to work with any "truncation location" string. Previous version only truncated at instance of string @" +". Edit allows you to define where you want the truncation to happen.


I took my answer from this question (which was an answer modified from the answer on this site) and tailored it to fit your needs. Create a new NSString interface where you can send your string to be custom-truncated.

NOTE: This solution is for iOS 7+ only. To use in iOS 6, use sizeWithFont: instead of sizeWithAttributes: in the NSString+TruncateToWidth.m file.

NSString+TruncateToWidth.h

@interface NSString (TruncateToWidth)
- (NSString*)stringByTruncatingAtString:(NSString *)string toWidth:(CGFloat)width withFont:(UIFont *)font;
@end

NSString+TruncateToWidth.m

#import "NSString+TruncateToWidth.h"

#define ellipsis @"…"

@implementation NSString (TruncateToWidth)

- (NSString*)stringByTruncatingAtString:(NSString *)string toWidth:(CGFloat)width withFont:(UIFont *)font
{
    // If the string is already short enough, or 
    // if the 'truncation location' string doesn't exist
    // go ahead and pass the string back unmodified.
    if ([self sizeWithAttributes:@{NSFontAttributeName:font}].width < width ||
        [self rangeOfString:string].location == NSNotFound)
        return self;

    // Create copy that will be the returned result
    NSMutableString *truncatedString = [self mutableCopy];

    // Accommodate for ellipsis we'll tack on the beginning
    width -= [ellipsis sizeWithAttributes:@{NSFontAttributeName:font}].width;

    // Get range of the passed string. Note that this only works to the first instance found,
    // so if there are multiple, you need to modify your solution
    NSRange range = [truncatedString rangeOfString:string];
    range.length = 1;

    while([truncatedString sizeWithAttributes:@{NSFontAttributeName:font}].width > width 
           && range.location > 0)
    {
        range.location -= 1;
        [truncatedString deleteCharactersInRange:range];
    }

    // Append ellipsis
    range.length = 0;
    [truncatedString replaceCharactersInRange:range withString:ellipsis];

    return truncatedString;
}

@end

Using it:

// Make sure to import the header file where you want to use it
myLabel.text = [@"Benjamin Walker Jackson + 2 authors" stringByTruncatingAtString:@" +" toWidth:myLabel.frame.size.width withFont:myLabel.font];
// Sample Result: Benjamin Walte... + 2 authors
Community
  • 1
  • 1
Stonz2
  • 6,306
  • 4
  • 44
  • 64
  • thanks for your response, I only have one issue with your solution, what if the author name contains '+' ? the string will be truncated prematurely than – Eyal Jul 16 '14 at 15:40
  • I just noticed you mention this in your comment, any ideas how to overcome this issue? – Eyal Jul 16 '14 at 15:42
  • 2
    never mind solved it by doing [truncatedString rangeOfString:@" +" options:NSBackwardsSearch]; – Eyal Jul 16 '14 at 15:46
  • `while` snippet should has one more condition: `range.location >= 0` – kientux Jul 18 '16 at 08:44
  • @kientux while that would make it safer to use, it would also end up doubly-truncating your string, as it would mean that eliminating everything before the passed string would still be too wide for the label. You'd end up with "... + 2 auth..." from the example above. Just something to keep in mind. – Stonz2 Jul 18 '16 at 13:06
0

UILabel cannot handle such truncate function other than under iOS 7. either you should truncate the string according to the fixed(calculated with uilabel font size) length yourself but that's a headache, or you can use two UILabels. 1st uilabel with fixed size for first author. it will auto truncate at the end as you know already. then the 2nd uilabel should be drawn exactly on the righ side of first label with of " + 2 other authors".

Edit: take a look at @Stonz2 answer

Hashmat Khalil
  • 1,826
  • 1
  • 25
  • 49
0

This answer is based on https://stackoverflow.com/a/30813691/2123122. Here is the sample code.

@interface CustomLabel()

  @property (nonatomic, retain) NSLayoutManager *layoutManager;
  @property (nonatomic, retain) NSTextContainer *textContainer;
  @property (nonatomic, retain) NSTextStorage *textStorage;

@end

@implementation CustomLabel

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
       self = [super initWithCoder:aDecoder];
       if (self) {
         [self configureTextkitStack];
       }
       return self;
 }
 - (instancetype)initWithFrame:(CGRect)frame {
       self = [super initWithFrame:frame];
       if (self) {
         [self configureTextkitStack];
       }
       return self;
  }

- (void)configureTextkitStack {
      _textContainer = [[NSTextContainer alloc] init];
      _textContainer.lineFragmentPadding = 0;
      _textContainer.maximumNumberOfLines = self.numberOfLines;
      _textContainer.lineBreakMode = self.lineBreakMode;
      _textContainer.widthTracksTextView = YES;

      _layoutManager = [[NSLayoutManager alloc] init];
      [_layoutManager addTextContainer:self.textContainer];

      [_textContainer setLayoutManager:self.layoutManager];

      _textStorage = [[NSTextStorage alloc] init];
      [_textStorage addLayoutManager:self.layoutManager];
      [self.layoutManager setTextStorage:_textStorage];
}

- (NSRange)rangeForTokenInsertion {
      self.textContainer.size = self.bounds.size;
      if (self.attributedText.length > 0 ){
        [self.textStorage setAttributedString:[[NSMutableAttributedString alloc]initWithAttributedString:self.attributedText]];
      }
      if (self.text.length == 0) {
        return NSMakeRange(NSNotFound, 0);
      }

      NSInteger glyphIndex = [self.layoutManager glyphIndexForCharacterAtIndex:self.textStorage.length - 1];
      NSRange range = [self.layoutManager truncatedGlyphRangeInLineFragmentForGlyphAtIndex:glyphIndex];

      return range;
    }

Now you can use this as follows:

 NSRange range = [self.label rangeForTokenInsertion];
 NSString *token = @"...+2 authors";
 if (range.location != NSNotFound ) {
  range.length += token.length;
  range.location -= token.length;
 }
 if (range.location != NSNotFound) {
  NSString *finalString = [self.label.text  stringByReplacingCharactersInRange:range withString:token];
  self.label.text = finalString;
 }

I have created a UILabel subclass called ResponsiveLabel which handles custom truncation token as well as applying styles to patterns like userhandles, URLs, hashtags etc.

Community
  • 1
  • 1
hsusmita
  • 282
  • 3
  • 16