Multi-line NSAttributedString with truncated text
Asked Answered
R

10

31

I need a UILabel subcass with multiline attributed text with support for links, bold styles, etc. I also need tail truncation with an ellipsis. None of the open source code that supports attributed text inside UILabels (TTTAttributedLabel, OHAttributedLabel, TTStyledTextLabel) seem to support tail truncation for multi-line text. Is there an easy way to get this?

Reginareginald answered 30/9, 2011 at 14:27 Comment(0)
U
15

Hi I am the developer of OHAttributedLabel.

There is no easy way to achieve this (as explained in the associated issue I opened on the github repository of my project), because CoreText does not offer such feature.

The only way to do this would be to implement the text layout yourself using CoreText objects (CTLine, etc) instead of using the CTFrameSetter that does this for you (but w/o managing line truncation). The idea would be to build all the CTLines to lay them out (depending on the glyphs in your NSAttributedString it contains and the word wrapping policy) one after the other and manage the ellipsis at the end yourself.

I would really appreciate if someone propose a solution to do this propery as it seems a bit of work to do and you have to manage a range of special/unusual cases too (emoji cases, fonts with odd metrics and unusual glyphs, vertical alignment, take into account the size of the ellipsis itself at the end to know when to stop).

So feel free to dig around and try to implement the framing of the lines yourself it would be really appreciated!

Universal answered 30/9, 2011 at 14:49 Comment(5)
Thanks so much for the response and the confirmation that this is the case as I had thought. Is there a problem with the simple and naive solution that would calculate whether truncation is needed and then simply remove 1 or 2 characters from the attributed string and add an ellipsis in their place?Reginareginald
That could work even if you will have to determine which characters to remove (at what offset does the text gets troncated — well that's the easiers part) and how much (that's more complicated) depending on the font and size at the truncation point (3 letters 'i' in plain text won't be enough but 3 'w' in bold will be too much…), and especially if the trunction happens in the middle of a change of style (font, style, size, …) too! Also the "…" ellipsis single character won't be the same size depending on the font too…, so even if this is possible, be careful with all those tricky casesUniversal
I haven't tried this but also I noticed in the CoreText documentation on manual line breaking the CTTypesetterSuggestLineBreak function which will suggest a line break on an attributed string for a given width. This width could be calculated on the last CTLine by calculating the width of the ellipsis with the same attributed string and it seems like most of your above special cases would then be handled properly.Reginareginald
Thks for the tip, seems promising. Note: if you have a github account, don't hesitate to post a comment on the related issue (or do a fork to try some code and send a pull request then)Universal
I think the error lays in the general assumption that line breaks and truncation are the same. In Core Text they are regarded as that - either you line break, truncate or clip. I think that is a wrong assumption. Line breaks are when you run out of width. Truncation is when you run out of height. I have submitted a feedback report. Others should do the same as the number of duplicate bug reports gives an issue higher priority.Sha
O
51

Maybe I'm missing something, but whats wrong with? :

NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"test"];

NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
style.lineBreakMode = NSLineBreakByTruncatingTail;
[text addAttribute:NSParagraphStyleAttributeName
                      value:style
                      range:NSMakeRange(0, text.length)];

label.attributedText = text;

This works perfectly and will add a ellipsis to the end.

Overlap answered 18/2, 2015 at 15:27 Comment(7)
Verified. Works perfectly, iOS 8.3 here.Seppuku
This should be the correct answer now. Previous to iOS6, UILabel didn't support attributed strings which is why this used to be a difficult issue.Contuse
@Overlap I am using tttattributed label for truncation. But if suppose \n is there in text then the truncated link is not clicked.So is there any way so that i can append my custom truncated token at the end of string if it is getting truncated.Precession
problem lineBreakMode don't permit to set up a custom ellipsis other than ... :(Congruous
That's because this answer is provided in Objective-C. See it as an exercise to convert it to Swift. :)Lascivious
You are missing the multiline part. When setting lineBreakMode to truncate, you are inherently preventing it from breaking.Sha
Is there a way to limit this to the first line of the paragraph?Sentimental
U
15

Hi I am the developer of OHAttributedLabel.

There is no easy way to achieve this (as explained in the associated issue I opened on the github repository of my project), because CoreText does not offer such feature.

The only way to do this would be to implement the text layout yourself using CoreText objects (CTLine, etc) instead of using the CTFrameSetter that does this for you (but w/o managing line truncation). The idea would be to build all the CTLines to lay them out (depending on the glyphs in your NSAttributedString it contains and the word wrapping policy) one after the other and manage the ellipsis at the end yourself.

I would really appreciate if someone propose a solution to do this propery as it seems a bit of work to do and you have to manage a range of special/unusual cases too (emoji cases, fonts with odd metrics and unusual glyphs, vertical alignment, take into account the size of the ellipsis itself at the end to know when to stop).

So feel free to dig around and try to implement the framing of the lines yourself it would be really appreciated!

Universal answered 30/9, 2011 at 14:49 Comment(5)
Thanks so much for the response and the confirmation that this is the case as I had thought. Is there a problem with the simple and naive solution that would calculate whether truncation is needed and then simply remove 1 or 2 characters from the attributed string and add an ellipsis in their place?Reginareginald
That could work even if you will have to determine which characters to remove (at what offset does the text gets troncated — well that's the easiers part) and how much (that's more complicated) depending on the font and size at the truncation point (3 letters 'i' in plain text won't be enough but 3 'w' in bold will be too much…), and especially if the trunction happens in the middle of a change of style (font, style, size, …) too! Also the "…" ellipsis single character won't be the same size depending on the font too…, so even if this is possible, be careful with all those tricky casesUniversal
I haven't tried this but also I noticed in the CoreText documentation on manual line breaking the CTTypesetterSuggestLineBreak function which will suggest a line break on an attributed string for a given width. This width could be calculated on the last CTLine by calculating the width of the ellipsis with the same attributed string and it seems like most of your above special cases would then be handled properly.Reginareginald
Thks for the tip, seems promising. Note: if you have a github account, don't hesitate to post a comment on the related issue (or do a fork to try some code and send a pull request then)Universal
I think the error lays in the general assumption that line breaks and truncation are the same. In Core Text they are regarded as that - either you line break, truncate or clip. I think that is a wrong assumption. Line breaks are when you run out of width. Truncation is when you run out of height. I have submitted a feedback report. Others should do the same as the number of duplicate bug reports gives an issue higher priority.Sha
E
9

Based on what I found here and over at https://groups.google.com/forum/?fromgroups=#!topic/cocoa-unbound/Qin6gjYj7XU, I came up with the following which works very well.

- (void)drawString:(CFAttributedStringRef)attString inRect:(CGRect)frameRect inContext:    (CGContextRef)context
{
CGContextSaveGState(context);

// Flip the coordinate system
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);

CGFloat height = self.frame.size.height;
frameRect.origin.y = (height - frameRect.origin.y)  - frameRect.size.height ;

// Create a path to render text in
// don't set any line break modes, etc, just let the frame draw as many full lines as will fit
CGMutablePathRef framePath = CGPathCreateMutable();
CGPathAddRect(framePath, nil, frameRect);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attString);
CFRange fullStringRange = CFRangeMake(0, CFAttributedStringGetLength(attString));
CTFrameRef aFrame = CTFramesetterCreateFrame(framesetter, fullStringRange, framePath, NULL);
CFRelease(framePath);

CFArrayRef lines = CTFrameGetLines(aFrame);
CFIndex count = CFArrayGetCount(lines);
CGPoint *origins = malloc(sizeof(CGPoint)*count);
CTFrameGetLineOrigins(aFrame, CFRangeMake(0, count), origins);

// note that we only enumerate to count-1 in here-- we draw the last line separately
for (CFIndex i = 0; i < count-1; i++)
{
    // draw each line in the correct position as-is
    CGContextSetTextPosition(context, origins[i].x + frameRect.origin.x, origins[i].y + frameRect.origin.y);
    CTLineRef line = (CTLineRef)CFArrayGetValueAtIndex(lines, i);
    CTLineDraw(line, context);
}

// truncate the last line before drawing it
if (count) {
    CGPoint lastOrigin = origins[count-1];
    CTLineRef lastLine = CFArrayGetValueAtIndex(lines, count-1);

    // truncation token is a CTLineRef itself
    CFRange effectiveRange;
    CFDictionaryRef stringAttrs = CFAttributedStringGetAttributes(attString, 0, &effectiveRange);

    CFAttributedStringRef truncationString = CFAttributedStringCreate(NULL, CFSTR("\u2026"), stringAttrs);
    CTLineRef truncationToken = CTLineCreateWithAttributedString(truncationString);
    CFRelease(truncationString);

    // now create the truncated line -- need to grab extra characters from the source string,
    // or else the system will see the line as already fitting within the given width and
    // will not truncate it.

    // range to cover everything from the start of lastLine to the end of the string
    CFRange rng = CFRangeMake(CTLineGetStringRange(lastLine).location, 0);
    rng.length = CFAttributedStringGetLength(attString) - rng.location;

    // substring with that range
    CFAttributedStringRef longString = CFAttributedStringCreateWithSubstring(NULL, attString, rng);
    // line for that string
    CTLineRef longLine = CTLineCreateWithAttributedString(longString);
    CFRelease(longString);

    CTLineRef truncated = CTLineCreateTruncatedLine(longLine, frameRect.size.width, kCTLineTruncationEnd, truncationToken);
    CFRelease(longLine);
    CFRelease(truncationToken);

    // if 'truncated' is NULL, then no truncation was required to fit it
    if (truncated == NULL)
        truncated = (CTLineRef)CFRetain(lastLine);

    // draw it at the same offset as the non-truncated version
    CGContextSetTextPosition(context, lastOrigin.x + frameRect.origin.x, lastOrigin.y + frameRect.origin.y);
    CTLineDraw(truncated, context);
    CFRelease(truncated);
}
free(origins);

CGContextRestoreGState(context);

}

Erek answered 7/2, 2013 at 16:30 Comment(4)
Could you explain, which class did you subclass?Pruritus
@Erek this code is leaking for me... should the CTFrameRef be released also?Jorum
@Erek adding CFRelease(aFrame); CFRelease(framesetter); at the end after free(origins) did the trick... no leaks!Jorum
Can we add Image to truncationToken string ? i mean attribute string with NSTextAttachment .?Monopoly
S
6

Heres a Swift 5 version of @Toydor's answer:

let attributedString = NSMutableAttributedString(string: "my String")    
let style: NSMutableParagraphStyle = NSMutableParagraphStyle()
style.lineBreakMode = .byTruncatingTail
attributedString.addAttribute(NSAttributedString.Key.paragraphStyle,
                                      value: style,
                                      range: NSMakeRange(0, attributedString.length))
Schechter answered 12/5, 2019 at 8:48 Comment(0)
N
3

I haven't tried this in all cases, but something like this could work for truncation:

NSAttributedString *string = self.attributedString;
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetTextMatrix(context, CGAffineTransformIdentity);

CFAttributedStringRef attributedString = (__bridge CFTypeRef)string;
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attributedString);
CGPathRef path = CGPathCreateWithRect(self.bounds, NULL);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);

BOOL needsTruncation = CTFrameGetVisibleStringRange(frame).length < string.length;
CFArrayRef lines = CTFrameGetLines(frame);
NSUInteger lineCount = CFArrayGetCount(lines);
CGPoint *origins = malloc(sizeof(CGPoint) * lineCount);
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);

for (NSUInteger i = 0; i < lineCount; i++) {
    CTLineRef line = CFArrayGetValueAtIndex(lines, i);
    CGPoint point = origins[i];
    CGContextSetTextPosition(context, point.x, point.y);

    BOOL truncate = (needsTruncation && (i == lineCount - 1));
    if (!truncate) {
        CTLineDraw(line, context);
    }
    else {
        NSDictionary *attributes = [string attributesAtIndex:string.length-1 effectiveRange:NULL];
        NSAttributedString *token = [[NSAttributedString alloc] initWithString:@"\u2026" attributes:attributes];
        CFAttributedStringRef tokenRef = (__bridge CFAttributedStringRef)token;
        CTLineRef truncationToken = CTLineCreateWithAttributedString(tokenRef);
        double width = CTLineGetTypographicBounds(line, NULL, NULL, NULL) - CTLineGetTrailingWhitespaceWidth(line);
        CTLineRef truncatedLine = CTLineCreateTruncatedLine(line, width-1, kCTLineTruncationEnd, truncationToken);

        if (truncatedLine) { CTLineDraw(truncatedLine, context); }
        else { CTLineDraw(line, context); }

        if (truncationToken) { CFRelease(truncationToken); }
        if (truncatedLine) { CFRelease(truncatedLine); }
    }
}

free(origins);
CGPathRelease(path);
CFRelease(frame);
CFRelease(framesetter);
Nedranedrah answered 30/1, 2013 at 20:0 Comment(1)
This seems to be working, but text is drawn upside-down. To prevent this the following lines of code need to be added after CGContextSetTextMatrix(context, CGAffineTransformIdentity); : CGContextTranslateCTM(context, 0.0f, rect.size.height); CGContextScaleCTM(context, 1.0f, -1.0f);Stoneman
S
2

Multi Line Vertical Glyph With Truncated. Swift3 and Swift4 version.
Add: Xcode9.1 Swift4 Compatibility. ( use block "#if swift(>=4.0)" )

class MultiLineVerticalGlyphWithTruncated: UIView, SimpleVerticalGlyphViewProtocol {

    var text:String!
    var font:UIFont!
    var isVertical:Bool!

    func setupProperties(text: String?, font:UIFont?, isVertical:Bool) {
        self.text = text ?? ""
        self.font = font ?? UIFont.systemFont(ofSize: UIFont.systemFontSize)
        self.isVertical = isVertical
    }


    override func draw(_ rect: CGRect) {
        if self.text == nil {
            return
        }

        // Create NSMutableAttributedString
        let attributed = NSMutableAttributedString(string: text) // if no ruby
        //let attributed = text.attributedStringWithRuby() // if with ruby, Please create custom method

    #if swift(>=4.0)
        attributed.addAttributes([
            NSAttributedStringKey.font: font,
            NSAttributedStringKey.verticalGlyphForm: isVertical,
            ],
                                 range: NSMakeRange(0, attributed.length))
    #else
        attributed.addAttributes([
            kCTFontAttributeName as String: font,
            kCTVerticalFormsAttributeName as String: isVertical,
            ],
                                 range: NSMakeRange(0, attributed.length))
    #endif

        drawContext(attributed, textDrawRect: rect, isVertical: isVertical)
    }
}

protocol SimpleVerticalGlyphViewProtocol {
}

extension SimpleVerticalGlyphViewProtocol {

    func drawContext(_ attributed:NSMutableAttributedString, textDrawRect:CGRect, isVertical:Bool) {

        guard let context = UIGraphicsGetCurrentContext() else { return }

        var path:CGPath
        if isVertical {
            context.rotate(by: .pi / 2)
            context.scaleBy(x: 1.0, y: -1.0)
            path = CGPath(rect: CGRect(x: textDrawRect.origin.y, y: textDrawRect.origin.x, width: textDrawRect.height, height: textDrawRect.width), transform: nil)
        }
        else {
            context.textMatrix = CGAffineTransform.identity
            context.translateBy(x: 0, y: textDrawRect.height)
            context.scaleBy(x: 1.0, y: -1.0)
            path = CGPath(rect: textDrawRect, transform: nil)
        }

        let framesetter = CTFramesetterCreateWithAttributedString(attributed)
        let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributed.length), path, nil)

        // Check need for truncate tail
        if (CTFrameGetVisibleStringRange(frame).length as Int) < attributed.length {

            // Required truncate

            let linesNS: NSArray  = CTFrameGetLines(frame)
            let linesAO: [AnyObject] = linesNS as [AnyObject]
            var lines: [CTLine] = linesAO as! [CTLine]

            let boundingBoxOfPath = path.boundingBoxOfPath


            let lastCTLine = lines.removeLast()

            let truncateString:CFAttributedString = CFAttributedStringCreate(nil, "\u{2026}" as CFString, CTFrameGetFrameAttributes(frame))
            let truncateToken:CTLine = CTLineCreateWithAttributedString(truncateString)

            let lineWidth = CTLineGetTypographicBounds(lastCTLine, nil, nil, nil)
            let tokenWidth = CTLineGetTypographicBounds(truncateToken, nil, nil, nil)
            let widthTruncationBegins = lineWidth - tokenWidth
            if let truncatedLine = CTLineCreateTruncatedLine(lastCTLine, widthTruncationBegins, .end, truncateToken) {
                lines.append(truncatedLine)
            }

            var lineOrigins = Array<CGPoint>(repeating: CGPoint.zero, count: lines.count)
            CTFrameGetLineOrigins(frame, CFRange(location: 0, length: lines.count), &lineOrigins)
            for (index, line) in lines.enumerated() {
                context.textPosition = CGPoint(x: lineOrigins[index].x + boundingBoxOfPath.origin.x, y:lineOrigins[index].y + boundingBoxOfPath.origin.y)
                CTLineDraw(line, context)
            }

        }
        else {
            // Not required truncate
            CTFrameDraw(frame, context)
        }
    }
}

How to use

class ViewController: UIViewController {

    @IBOutlet weak var multiLineVerticalGlyphWithTruncated: MultiLineVerticalGlyphWithTruncated! // UIView

    let font:UIFont = UIFont(name: "HiraMinProN-W3", size: 17.0) ?? UIFont.systemFont(ofSize: 17.0)

    override func viewDidLoad() {
        super.viewDidLoad()

        let text = "iOS 11 sets a new standard for what is already the world’s most advanced mobile operating system. It makes iPhone better than before. It makes iPad more capable than ever. And now it opens up both to amazing possibilities for augmented reality in games and apps. With iOS 11, iPhone and iPad are the most powerful, personal, and intelligent devices they’ve ever been."
        // If check for Japanese
//        let text = "すでに世界で最も先進的なモバイルオペレーティングシステムであるiOSに、iOS 11が新たな基準を打ち立てます。iPhoneは今まで以上に優れたものになり、iPadはかつてないほどの能力を手に入れます。さらにこれからはどちらのデバイスにも、ゲームやアプリケーションの拡張現実のための驚くような可能性が広がります。iOS 11を搭載するiPhoneとiPadは、間違いなくこれまでで最もパワフルで、最もパーソナルで、最も賢いデバイスです。"

        // if not vertical text, isVertical = false
        multiLineVerticalGlyphWithTruncated.setupProperties(text: text, font: font, isVertical: true)
    }
}
Synchromesh answered 7/9, 2017 at 7:52 Comment(0)
S
1

You may be able to use follow code to have a more simple solution.

    // last line.
    if (_limitToNumberOfLines && count == _numberOfLines-1)
    {
        // check if we reach end of text.
        if (lineRange.location + lineRange.length < [_text length])
        {
            CFDictionaryRef dict = ( CFDictionaryRef)attributes;
            CFAttributedStringRef truncatedString = CFAttributedStringCreate(NULL, CFSTR("\u2026"), dict);

            CTLineRef token = CTLineCreateWithAttributedString(truncatedString);

            // not possible to display all text, add tail ellipsis.
            CTLineRef truncatedLine = CTLineCreateTruncatedLine(line, self.bounds.size.width - 20, kCTLineTruncationEnd, token);
            CFRelease(line); line = nil;
            line = truncatedLine;
        }
    }

I'm using MTLabel in my project and it's a really nice solution for my project.

Sunshade answered 16/4, 2012 at 10:56 Comment(0)
S
0

I integrated wbyoung's solution into OHAttributedLabel drawTextInRect: method if anyone is interested:

- (void)drawTextInRect:(CGRect)aRect
{
    if (_attributedText)
    {
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGContextSaveGState(ctx);

    // flipping the context to draw core text
    // no need to flip our typographical bounds from now on
    CGContextConcatCTM(ctx, CGAffineTransformScale(CGAffineTransformMakeTranslation(0, self.bounds.size.height), 1.f, -1.f));

    if (self.shadowColor)
    {
        CGContextSetShadowWithColor(ctx, self.shadowOffset, 0.0, self.shadowColor.CGColor);
    }

    [self recomputeLinksInTextIfNeeded];
    NSAttributedString* attributedStringToDisplay = _attributedTextWithLinks;
    if (self.highlighted && self.highlightedTextColor != nil)
    {
        NSMutableAttributedString* mutAS = [attributedStringToDisplay mutableCopy];
        [mutAS setTextColor:self.highlightedTextColor];
        attributedStringToDisplay = mutAS;
        (void)MRC_AUTORELEASE(mutAS);
    }
    if (textFrame == NULL)
    {
        CFAttributedStringRef cfAttrStrWithLinks = (BRIDGE_CAST CFAttributedStringRef)attributedStringToDisplay;
        CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(cfAttrStrWithLinks);
        drawingRect = self.bounds;
        if (self.centerVertically || self.extendBottomToFit)
        {
            CGSize sz = CTFramesetterSuggestFrameSizeWithConstraints(framesetter,CFRangeMake(0,0),NULL,CGSizeMake(drawingRect.size.width,CGFLOAT_MAX),NULL);
            if (self.extendBottomToFit)
            {
                CGFloat delta = MAX(0.f , ceilf(sz.height - drawingRect.size.height))+ 10 /* Security margin */;
                drawingRect.origin.y -= delta;
                drawingRect.size.height += delta;
            }
            if (self.centerVertically) {
                drawingRect.origin.y -= (drawingRect.size.height - sz.height)/2;
            }
        }
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, drawingRect);
        CFRange fullStringRange = CFRangeMake(0, CFAttributedStringGetLength(cfAttrStrWithLinks));
        textFrame = CTFramesetterCreateFrame(framesetter,fullStringRange, path, NULL);
        CGPathRelease(path);
        CFRelease(framesetter);
    }

    // draw highlights for activeLink
    if (_activeLink)
    {
        [self drawActiveLinkHighlightForRect:drawingRect];
    }

    BOOL hasLinkFillColorSelector = [self.delegate respondsToSelector:@selector(attributedLabel:fillColorForLink:underlineStyle:)];
    if (hasLinkFillColorSelector) {
        [self drawInactiveLinkHighlightForRect:drawingRect];
    }

    if (self.truncLastLine) {
        CFArrayRef lines = CTFrameGetLines(textFrame);
        CFIndex count = MIN(CFArrayGetCount(lines),floor(self.size.height/self.font.lineHeight));

        CGPoint *origins = malloc(sizeof(CGPoint)*count);
        CTFrameGetLineOrigins(textFrame, CFRangeMake(0, count), origins);

        // note that we only enumerate to count-1 in here-- we draw the last line separately

        for (CFIndex i = 0; i < count-1; i++)
        {
            // draw each line in the correct position as-is
            CGContextSetTextPosition(ctx, origins[i].x + drawingRect.origin.x, origins[i].y + drawingRect.origin.y);
            CTLineRef line = (CTLineRef)CFArrayGetValueAtIndex(lines, i);
            CTLineDraw(line, ctx);
        }

        // truncate the last line before drawing it
        if (count) {
            CGPoint lastOrigin = origins[count-1];
            CTLineRef lastLine = CFArrayGetValueAtIndex(lines, count-1);

            // truncation token is a CTLineRef itself
            CFRange effectiveRange;
            CFDictionaryRef stringAttrs = CFAttributedStringGetAttributes((BRIDGE_CAST CFAttributedStringRef)_attributedTextWithLinks, 0, &effectiveRange);

            CFAttributedStringRef truncationString = CFAttributedStringCreate(NULL, CFSTR("\u2026"), stringAttrs);
            CTLineRef truncationToken = CTLineCreateWithAttributedString(truncationString);
            CFRelease(truncationString);

            // now create the truncated line -- need to grab extra characters from the source string,
            // or else the system will see the line as already fitting within the given width and
            // will not truncate it.

            // range to cover everything from the start of lastLine to the end of the string
            CFRange rng = CFRangeMake(CTLineGetStringRange(lastLine).location, 0);
            rng.length = CFAttributedStringGetLength((BRIDGE_CAST CFAttributedStringRef)_attributedTextWithLinks) - rng.location;

            // substring with that range
            CFAttributedStringRef longString = CFAttributedStringCreateWithSubstring(NULL, (BRIDGE_CAST CFAttributedStringRef)_attributedTextWithLinks, rng);
            // line for that string
            CTLineRef longLine = CTLineCreateWithAttributedString(longString);
            CFRelease(longString);

            CTLineRef truncated = CTLineCreateTruncatedLine(longLine, drawingRect.size.width, kCTLineTruncationEnd, truncationToken);
            CFRelease(longLine);
            CFRelease(truncationToken);

            // if 'truncated' is NULL, then no truncation was required to fit it
            if (truncated == NULL){
                truncated = (CTLineRef)CFRetain(lastLine);
            }

            // draw it at the same offset as the non-truncated version
            CGContextSetTextPosition(ctx, lastOrigin.x + drawingRect.origin.x, lastOrigin.y + drawingRect.origin.y);
            CTLineDraw(truncated, ctx);
            CFRelease(truncated);
        }
        free(origins);
        }
         else{
            CTFrameDraw(textFrame, ctx);
         }

        CGContextRestoreGState(ctx);
    } else {
        [super drawTextInRect:aRect];
        }
}
Sap answered 20/3, 2014 at 10:9 Comment(0)
C
0

Ran into this issue here in 2021.

I wrapped @Toydor's and @gypsyDev's answer into a UILabel extension so it can be applied where needed. Swift 5.

extension UILabel {
    func ensureAttributedTextShowsTruncation() {
    
        guard let text = attributedText else { return }
    
        let attributedString = NSMutableAttributedString(attributedString: text)

        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.lineBreakMode = .byTruncatingTail
    
        attributedString.addAttribute(.paragraphStyle,
                                  value: paragraphStyle,
                                  range: NSRange(location: 0, length: attributedString.length))

        attributedText = attributedString
    }
}
Colosseum answered 24/8, 2021 at 21:51 Comment(0)
B
-1

I used as sample MTLabel. It allows to manage line height. I needed the draw method exactly, so i just put away most stuff i did not need. This method allows me to draw multilined text in rect with tail truncation.

CGRect CTLineGetTypographicBoundsAsRect(CTLineRef line, CGPoint lineOrigin)
{
CGFloat ascent = 0;
CGFloat descent = 0;
CGFloat leading = 0;
CGFloat width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
CGFloat height = ascent + descent;

return CGRectMake(lineOrigin.x,
                  lineOrigin.y - descent,
                  width,
                  height);
}
- (void)drawText:(NSString*) text InRect:(CGRect)rect withFont:(UIFont*)aFont inContext:(CGContextRef)context {

if (!text) {
    return;
}

BOOL _limitToNumberOfLines = YES;
int _numberOfLines = 2;
float _lineHeight = 22;

//Create a CoreText font object with name and size from the UIKit one
CTFontRef font = CTFontCreateWithName((CFStringRef)aFont.fontName ,
                                      aFont.pointSize,
                                      NULL);


//Setup the attributes dictionary with font and color
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
                            (id)font, (id)kCTFontAttributeName,
                            [UIColor lightGrayColor].CGColor, kCTForegroundColorAttributeName,
                            nil];

NSAttributedString *attributedString = [[[NSAttributedString alloc]
                                         initWithString:text
                                         attributes:attributes] autorelease];

CFRelease(font);

//Create a TypeSetter object with the attributed text created earlier on
CTTypesetterRef typeSetter = CTTypesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);

//Start drawing from the upper side of view (the context is flipped, so we need to grab the height to do so)
CGFloat y = self.bounds.origin.y + self.bounds.size.height - rect.origin.y - aFont.ascender;

BOOL shouldDrawAlong = YES;
int count = 0;
CFIndex currentIndex = 0;

float _textHeight = 0;

CGContextSaveGState(context);

CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);

//Start drawing lines until we run out of text
while (shouldDrawAlong) {

    //Get CoreText to suggest a proper place to place the line break
    CFIndex lineLength = CTTypesetterSuggestLineBreak(typeSetter,
                                                      currentIndex,
                                                      rect.size.width);

    //Create a new line with from current index to line-break index
    CFRange lineRange = CFRangeMake(currentIndex, lineLength);
    CTLineRef line = CTTypesetterCreateLine(typeSetter, lineRange);

    //Check to see if our index didn't exceed the text, and if should limit to number of lines        
    if (currentIndex + lineLength >= [text length])
    {
        shouldDrawAlong = NO;
    }
    else
    {
        if (!(_limitToNumberOfLines && count < _numberOfLines-1))
        {
            int i = 0;
            if ([[[text substringWithRange:NSMakeRange(currentIndex, lineLength)] stringByAppendingString:@"…"] sizeWithFont:aFont].width > rect.size.width)
            {
                i--;
                while ([[[text substringWithRange:NSMakeRange(currentIndex, lineLength + i)] stringByAppendingString:@"…"] sizeWithFont:aFont].width > rect.size.width) 
                {
                    i--;
                }
            }
            else
            {
                i++;
                while ([[[text substringWithRange:NSMakeRange(currentIndex, lineLength + i)] stringByAppendingString:@"…"] sizeWithFont:aFont].width < rect.size.width) 
                {
                    i++;
                }
                i--;
            }
            attributedString = [[[NSAttributedString alloc] initWithString:[[text substringWithRange:NSMakeRange(currentIndex, lineLength + i)] stringByAppendingString:@"…"] attributes:attributes] autorelease];

            CFRelease(typeSetter);

            typeSetter = CTTypesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);

            CFRelease(line);

            CFRange lineRange = CFRangeMake(0, 0);
            line = CTTypesetterCreateLine(typeSetter, lineRange);

            shouldDrawAlong = NO;
        }
    }


    CGFloat x = rect.origin.x;
    //Setup the line position
    CGContextSetTextPosition(context, x, y);
    CTLineDraw(line, context);

    count++;
    CFRelease(line);

    y -= _lineHeight;

    currentIndex += lineLength;
    _textHeight += _lineHeight;
}

CFRelease(typeSetter);

CGContextRestoreGState(context);

}
Babbage answered 5/3, 2012 at 16:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.