Search code examples
iosnsstringios9nsattributedstring

Use something else than ellipsis (...) with NSStringDrawingTruncatesLastVisibleLine


I am trying to render some text in background using [NSAttributedString drawWithRect:options:context:] method and I'm passing (NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading| NSStringDrawingTruncatesLastVisibleLine | NSLineBreakByWordWrapping) for the options. If my string is longer than two lines (I've calculated the max height of the rectangle for that) my text is truncated with ....

It works great, however, instead of ..., I need to truncate with ...more (with "more" at the end).

All the rendering must be done on background thread so any UI component is not possible. And please don't recommend TTTAttributedLabel because I'm trying to get away from it in the first place as it resulted in terrible performance in scrolling in my app (already tried that).

How can I use a custom truncation token when drawing a string in background thread?


Solution

  • May not be the most efficient thing, but I've ended up like this:

    • Check the size of the string with desired width and no height limit (using MAXFLOAT for the bounding rect's height in draw method):

      NSStringDrawingOptions drawingOptions = (NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading); [stringToDraw boundingRectWithSize:maximumSize options:drawingOptions context:nil].size;

    • As I know the font size, check the height of the resulting size and check if it's taller than a predetermined height which would indicate if it's more than two lines.

    • If it's more than two lines, get the index of the character at the point of the rectangle where ...more should roughly start, using a modified version of the answer at https://stackoverflow.com/a/26806991/811405 (the point is somewhere near the bottom right of the original text's rectangle's second line):

      //this string spans more than two lines.
      NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString];
      NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
      [textStorage addLayoutManager:layoutManager];
      // init text container
      NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:size];
      textContainer.lineFragmentPadding  = 0;
      textContainer.maximumNumberOfLines = 2;
      textContainer.lineBreakMode        = NSLineBreakByClipping;
      [layoutManager addTextContainer:textContainer];
      CGPoint moreStartLocation = CGPointMake(size.width - 60, 30); //35 magic number
      NSUInteger characterIndex = [layoutManager characterIndexForPoint:moreStartLocation
                                                        inTextContainer:textContainer
                               fractionOfDistanceBetweenInsertionPoints:NULL];
      stringToDraw = [attributedString attributedSubstringFromRange:NSMakeRange(0, characterIndex)].mutableCopy;
      [stringToDraw appendAttributedString:self.truncationText];
      size = CGSizeMake(size.width, 35);
      
    • Truncate the original string to character there (optional: one can also find the last whitespace (e.g. space, newline) character from the limit and get the substring from that point to avoid word clipping). Add the "...more" to the original string. (The text can be anything, with any attributes. Just make sure it will fit into the result rectangle in two lines. I've fixed it to 60px, but one can also get the size of their desired truncation string, and use its width to find the last character precisely)

    • Render the new string (ending with "...more") as usual:

      UIGraphicsBeginImageContextWithOptions(contextSize, YES, 0);
      [stringToDraw drawWithRect:rectForDrawing options:drawingOptions context:nil];
      UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
      UIGraphicsEndImageContext();
      

    The good thing about this method is that, since we're not touching UIKit (UIGraphics... functions and UIImage are thread-safe) we can execute the whole process in a background thread/queue. I'm using it to prerender some text content with attributes/links etc in background, that otherwise takes a frame or two in UI thread when scrolling, and it works perfectly.