Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UILineBreakMode*Truncation modes (leading/trailing "…") and multiline #3

Open
AliSoftware opened this issue Feb 18, 2011 · 22 comments

Comments

@AliSoftware
Copy link
Owner

Word Wrapping and Truncation is not well managed.

Today the OHAttributed does not support both multiline (numberOfLines>1) and "Truncation" lineBreakMode alltogether : if you choose one of the UILineBreakMode*Truncation mode, only the first line will be displayed.

It is probably easy to add the UILineBreakModeTailTruncation mode support by using the VisibleStringRange information (I just don't have time to add this right now)

@gon
Copy link

gon commented May 27, 2011

Hi!

This library is a great contribution, thanks so much for putting this together!
Can you give a little more indication how to implement this?

Gon

@AliSoftware
Copy link
Owner Author

Hi @gon

I didn't check in the documentation yet, but contrary to what I stated at the date of registering this issue, this won't probably be as trick as it seems: we need to take care of the truncation in the drawing method, but also in the link detection case.

You will probably need to use CTFramesetterSuggestFrameSizeWithConstraints to get the fitRange back, and/or more probably the CTFrameGetVisibleStringRange function. Thus, during drawing, I guess you will need to disable the lineBreakMode, compute the range of the string that will be able to be visible (using the methods mentioned above), then remplace the last visible character by the ellipsis character (and check this ellipsis char is visible, or else remove another one char at the end until it is), before actually drawing this modified string.

Or maybe there is a better, easier way to do this (I didn't check the doc fully either, neither did I google about it), especially I wonder what are the values that can be used in the frameAttributes parameter of the CTFrameRef CTFramesetterCreateFrame method, but obviously it is likely that this would be the place where we expect to provide lineBreaking info… if Apple did actually think about providing such possibility of line truncation?

@AliSoftware
Copy link
Owner Author

I also wonder if it is not in the NSAttributedString itself that we need to define the line truncation mode?
Maybe forget everything about CTFrame and CTFramesetter for lineBreaing issues, and check the NSAttributedString attributes instead?

@jdandrea
Copy link

Greetings! Is this still an issue? I'm trying to use OHAttributedLabel as the view in my navigationItem's titleView (similar to the iPod app's Now Playing, but two lines instead of three). I'm using kCTCenterTextAlignment and kCTLineBreakByTruncatingTail, then I call sizeToFit. Unfortunately, it doesn't resize to the width I expect, and so the ellipsis kicks in. Trying to figure out a sane way around this ...

@AliSoftware
Copy link
Owner Author

Hi @jdandrea

Unfortunately this is still an issue and I didn't have time to check it out so far.
Any help is appreciated on this one (even clues) as it does not seem to be as tricky as I thought and won't have much time to care about it until a while.

Thanks!

@jdandrea
Copy link

Ahh, understood. For now, I ended up increasing the width by 1.0 and ... the text fits! Of course this may be by chance. I'll have to try it with a few different bits of text and see. Where it might get interesting is if the view ends up being too wide. Not sure if the nav bar ratchets that down a bit automagically.

@AliSoftware
Copy link
Owner Author

Actually I think I understood your problem: sizeToFit may resize the label with a smaller width than the original one.
E.g. even if you label was originally, say 500px wide but its text is as short as "Hi", sizeToFit will resize it with a width of say 40px. As the text is centered, you may instead call sizeThatFits and get the returned CGSize but only use the height and don't use the width, so that you can keep the same width (wide enough) whatever the text and only adjust the height.

@jdandrea
Copy link

Oh! That's an interesting thought. In my case, I'm starting with CGRectZero (perhaps that's the problem right there?). Since this is going into the titleView on a nav bar's nav item, I'm not sure what width would work best. Perhaps I should start with a max width after all, and see if the nav bar ends up adjusting it to fit. Thanks for the tip! I'll check back with results.

@jdandrea
Copy link

jdandrea commented Aug 2, 2011

I didn't even have to do that. I just kept the width one greater than it already was, and the nav bar does the rest.

@AliSoftware
Copy link
Owner Author

Hi

My latest commit includes flooring the values returned by sizeToFit (to avoid subpixelling issues pointed out by issue #28) but also adding a +1 margin for the width to.
Don't hesitate to test with this new commit/version

@jdandrea
Copy link

jdandrea commented Aug 4, 2011

Thanks! That worked. :)

@AliSoftware AliSoftware reopened this Aug 18, 2011
@AliSoftware
Copy link
Owner Author

Wooops closed the issue by mistake (your sizeToFit issue is solved/closed by my last comment... but this issue #3 itself is related to "UILineBreakMode*Truncation" and should not be closed). Reopening.

@joshuatbrown
Copy link

Is there a workaround for this? I'd like to prevent the warning from showing up in my log - any line break mode is fine for me, as long as there's no warning in my debug console. :)

@AliSoftware
Copy link
Owner Author

@joshuatbrown Errr I don't understand your question… if any line break mode is fine for you, why don't you choose any line break mode that is not a UILineBreakModeXXXTrunctation (line UILineBreakModeWordWrap for example) to avoid the warning in the console?

This is even explained in the warning itself!!

"UILineBreakMode...Truncation" lineBreakModes are not yet fully supported [...]
To avoid this warning, change this property in your XIB file to another lineBreakMode value

@joshuatbrown
Copy link

My version of OHAttributedLabel does not produce the message with the solution, nor does the solution work. I still get the warning.

@AliSoftware
Copy link
Owner Author

So which version do you use, and which message do you get?
Be sure to be up-to-date to have the latest bugfixes and improvements… and the right warning messages.
You should at least update to version 2.0.0 which contains many bug fixes and speed & memory optimisations.


The warning in the latest versions always display the part "UILineBreakMode...Truncation" lineBreakModes are not yet fully supported [...] anyway, to let you know that the Truncation lineBreakModeyou use is unsupported and that you should use another, supported value.
This part seems pretty clear to me, that is you have this warning this is because you use an unsupported …Truncation value for this property (and that the truncation of your text will not behave as expected if you keep this value, as CoreText does not support this configuration). And that if you want to get rid of the warning you should use a supported value (instead of an unsupported one and risk unwanted behavior).
If it does not seem clear to you, please suggest how you would have explained it!

The part "change this property in your XIB file" in the warning is only displayed if your OHAttributedLabel instance has been created using a XIB (or storyboard) and not via code, as it is common to forget to change those properties in the Inspector Panel when you design your XIB, and is just an additional hint. Of course if you created your OHAttributedLabelby code, you will have to change the lineBreakMode property by code to set it ton a supported value (and you probably have a line that change the lineBreakMode of your OHAttributedLabel already in your code — but with an unsupported value —, as the default value when you don't set it is to Word Wrap and not Truncate, if I remember correctly?)

@wbyoung
Copy link

wbyoung commented Jan 30, 2013

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

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);

@AliSoftware
Copy link
Owner Author

Thx I'll try that probably this weekend. 👍

To be honest I was kinda "afraid" to implement layout of CTLines myself, not because of the complexity of the code (pretty basic for standard case), but because I suspected that the CTFrameDraw function makes some more work than just a simple loop, especially for truncation… so I guess that I would need to make this new manual layout handle all possible cases (text alignments, mixed paragraph styles and all) to test if we are handling all tricky cases.

The bad news is that even if we draw the lines ourselves like with your code, we will have a lot more to manage.
For example, your code will only manage Tail Truncation, and only if the NSAttributedString does not have ranges (CTRuns) that contains the truncation attribute on some of their paragraphs, etc. Especially, it won't handle middle truncation or head truncation and stuff like that :-/

So that's a step forward, sure (and I'll probably try and integrate it anyway as it's better than nothing), but unfortunately that does only solve the basic case and tail truncation… (Apple when are you gonna fix this?)

@mflint
Copy link

mflint commented Jul 2, 2013

Hmm, I can't figure out how to set the lineBreakMode property in a storyboard. Can anyone advise?

(I'm sure it'll be very obvious!)

@AliSoftware
Copy link
Owner Author

@mflint
UILabel Inspector Panel

@mflint
Copy link

mflint commented Jul 4, 2013

Thank-you @AliSoftware! I'd added the view to the storyboard as a "View", not as a "Label", so those properties were not available.

@eventomer
Copy link

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];
        }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants