CTFramesetterSuggestFrameSizeWithConstraints cuts off text

  • I'm using Core Text to layout text in an UIView but am having troubles with getting the correct size that the text needs, so I can set the contentSize of the UIScrollView that it is embedded in.  I know it is the wrong size, because the last line is not showing.

    I've done some spitting around, and it seems that CTFramesetterSuggestFrameSizeWithConstraints may not always work properly.  However I haven't found a solution that works for me.

    I've pasted my code below, maybe I am overlooking something?

    Thanks,

    - Koen.

    =============================

    // create the string:

        NSString *testString = @"This is a not so very long but at least long enough test string to cover more than one line.";

        CFMutableAttributedStringRef attrString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
        CFAttributedStringReplaceString (attrString, CFRangeMake(0, 0), (CFStringRef) testString );
        CFIndex stringLength = CFStringGetLength((CFStringRef) attrString);

    // give it a font

        UIFont* uiFont = [UIFont fontWithName: @"Courier" size: 22.0];
        CTFontRef ctFont = CTFontCreateWithName((__bridge CFStringRef) uiFont.fontName, uiFont.pointSize, NULL);
        CFAttributedStringSetAttribute(attrString, CFRangeMake(0, stringLength), kCTFontAttributeName, ctFont);

    // calculate the size it occupies

        CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attrString);
        CFRange fitRange;
        CGSize frameSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, stringLength), NULL, CGSizeMake(self.bounds.size.width, CGFLOAT_MAX), &fitRange);
        self.frame = CGRectMake(0, 0, self.frame.size.width, frameSize.height);  // <<<<====  frameSize.height is too small


    // draw the string:

        CGContextRef context = UIGraphicsGetCurrentContext();

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

        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, self.frame);

        CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef) attrString);
        CTFrameRef frameRef = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, stringLength), path, NULL);

        CTFrameDraw(frameRef, context);
  • On 17 May 2013, at 8:38 AM, Koen van der Drift <koenvanderdrift...> wrote:

    > CTFramesetterSuggestFrameSizeWithConstraints

    I ran into this problem and had to resort to an ad-hoc kludge (though I guess all kludges are ad-hoc:

    lineHeight = CTFontGetSize(self.ctDefaultFont)*1.2;
    ...
    frameSize = CTFramesetterSuggestFrameSizeWithConstraints(ctFramesetter...
    ...
    //  I wish I had a principled reason why this is necessary, or the best solution.
    //  It looks good from a cursory examination, but surely this fails in some cases,
    //  or with some combination of styles.
    frameSize.height += lineHeight / 2.0;

    — F

    --
    Fritz Anderson
    Xcode 4 Unleashed: 4.5 supplement for free!
    http://www.informit.com/store/xcode-4-unleashed-9780672333279
  • Also, I had another app that didn't need the kludge. I don't have time to experiment, but I think the operative line was:

        // boundingRect.size was {columwidth, CGFLOAT_MAX}
        suggestion = CTFramesetterSuggestFrameSizeWithConstraints(setter,
                                                                  CFRangeMake(0, 0),
                                                                  NULL,
                                                                  boundingRect.size,
                                                                  &actualRange);

    Note the range of {0,0}, which allows the framesetter to run until it runs out of space or string. That app worked properly.

    — F

    --
    Fritz Anderson
    Xcode 4 Unleashed: 4.5 supplement for free!
    http://www.informit.com/store/xcode-4-unleashed-9780672333279
  • Thanks Fritz,

    After trying out various values, this worked for me:

        CGSize frameSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter,
              CFRangeMake(0, 0),
              NULL,
              CGSizeMake(self.bounds.size.width, CGFLOAT_MAX),
              &fitRange);

        CGFloat lineHeight = CTFontGetSize( ctFont ) * 2.0;
        frameSize.height += lineHeight;

    I'm using CGFLOAT_MAX, since the text can be longer than the bounds of the view.

    - Koen.

    On May 17, 2013, at 10:37 AM, Fritz Anderson <fritza...> wrote:

    > Also, I had another app that didn't need the kludge. I don't have time to experiment, but I think the operative line was:
    >
    > // boundingRect.size was {columwidth, CGFLOAT_MAX}
    > suggestion = CTFramesetterSuggestFrameSizeWithConstraints(setter,
    > CFRangeMake(0, 0),
    > NULL,
    > boundingRect.size,
    > &actualRange);
    >
    > Note the range of {0,0}, which allows the framesetter to run until it runs out of space or string. That app worked properly.
    >
    > — F
    >
    >
    > --
    > Fritz Anderson
    > Xcode 4 Unleashed: 4.5 supplement for free!
    > http://www.informit.com/store/xcode-4-unleashed-9780672333279
    >
  • On May 17, 2013, at 06:38 , Koen van der Drift <koenvanderdrift...> wrote:

    > CGSize frameSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, stringLength), NULL, CGSizeMake(self.bounds.size.width, CGFLOAT_MAX), &fitRange);
    > self.frame = CGRectMake(0, 0, self.frame.size.width, frameSize.height);  // <<<<====  frameSize.height is too small

    I ran into this problem a while back, and after a lot of anguish I think I worked out what was going wrong. From an admittedly imperfect memory…

    'CTFramesetterSuggestFrameSizeWithConstraints' sometimes returns a width that's *larger* than the width in the 'constraints' parameter -- wider than the view width in your case. So, with your existing code, when the string is eventually typeset and displayed, your view isn't wide enough for the text to line-wrap with exactly the same line breaks as it when you got the suggested frame size, and the text wraps with (generally) one extra line.

    The extra width in the returned value comes from "hanging spaces" -- spaces at the end of a line that don't count in the line's wrap width. For example, if you specify a constraint width of 350 points, and the width metric for a space is 5 points (in the font size being used), then the longest possible typeset line is actually 355 points. There may also be other typesetting situations, such as hanging punctuation, which produce similar results.

    This is correct behavior, because the non-space characters in the line are allowed to use all 350 points of the width. (Hanging punctuation may partially occupy some of the 350-point width, and partially extend past the end of the width.)

    Note that the larger width isn't *always* returned -- it depends how close the non-hanging characters of the longest line come to the constraint width -- so the problem appears to be intermittent.

    It's disputable, I guess, but I convinced myself it's correct to return a width >350, because the character that "oversets" the line may have a non-blank glyph -- even a space, when displaying text with a "show invisibles" option -- and so needs to be given a place to display.

    Therefore, you need to use the *returned* width for your CTFrame, instead of your view width. This will mean that the CTFrame may overhang the view on the right. In most cases this won't matter, since in most cases the overhanging glyph will be a blank space.

    Alternatively, you can pass a smaller width to 'CTFramesetterSuggestFrameSizeWithConstraints' -- for example: self.frame.size.width - (width of a space) -- to ensure that hanging characters have a place to display. But this is an awkward solution if you have a mix of fonts and point sizes in your attributed string.
  • I ran into this few years ago and didn't find correct solution. Correct means nice & clean. Ended with adding 1/2 of line height, which causes another issues like wrong vertical alignment of elements. It was pre NSAttributedString era on iOS. Height was wrong even if returned width <= constrained width.

    Did spend on this couple of weeks and finally gave up.

    On Friday, 17. May 2013 at 19:25, Quincey Morris wrote:

    > On May 17, 2013, at 06:38 , Koen van der Drift <koenvanderdrift...> wrote:
    >
    >> CGSize frameSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, stringLength), NULL, CGSizeMake(self.bounds.size.width, CGFLOAT_MAX), &fitRange);
    >> self.frame = CGRectMake(0, 0, self.frame.size.width, frameSize.height); // <<<<==== frameSize.height is too small
    >>
    >
    >
    > I ran into this problem a while back, and after a lot of anguish I think I worked out what was going wrong. From an admittedly imperfect memory…
    >
    > 'CTFramesetterSuggestFrameSizeWithConstraints' sometimes returns a width that's *larger* than the width in the 'constraints' parameter -- wider than the view width in your case. So, with your existing code, when the string is eventually typeset and displayed, your view isn't wide enough for the text to line-wrap with exactly the same line breaks as it when you got the suggested frame size, and the text wraps with (generally) one extra line.
    >
    > The extra width in the returned value comes from "hanging spaces" -- spaces at the end of a line that don't count in the line's wrap width. For example, if you specify a constraint width of 350 points, and the width metric for a space is 5 points (in the font size being used), then the longest possible typeset line is actually 355 points. There may also be other typesetting situations, such as hanging punctuation, which produce similar results.
    >
    > This is correct behavior, because the non-space characters in the line are allowed to use all 350 points of the width. (Hanging punctuation may partially occupy some of the 350-point width, and partially extend past the end of the width.)
    >
    > Note that the larger width isn't *always* returned -- it depends how close the non-hanging characters of the longest line come to the constraint width -- so the problem appears to be intermittent.
    >
    > It's disputable, I guess, but I convinced myself it's correct to return a width >350, because the character that "oversets" the line may have a non-blank glyph -- even a space, when displaying text with a "show invisibles" option -- and so needs to be given a place to display.
    >
    > Therefore, you need to use the *returned* width for your CTFrame, instead of your view width. This will mean that the CTFrame may overhang the view on the right. In most cases this won't matter, since in most cases the overhanging glyph will be a blank space.
    >
    > Alternatively, you can pass a smaller width to 'CTFramesetterSuggestFrameSizeWithConstraints' -- for example: self.frame.size.width - (width of a space) -- to ensure that hanging characters have a place to display. But this is an awkward solution if you have a mix of fonts and point sizes in your attributed string.
  • On May 17, 2013, at 11:20 , Robert Vojta <robert...> wrote:

    > Height was wrong even if returned width <= constrained width.

    Once I took proper account of the returned width, I got no more "wrong" heights.

    However, IIRC, there was a separate issue that fractional line heights didn't work correctly. Maybe you ran into that problem as well.
  • Thanks all for the insightful responses. In my case I am using a monospaced
    font, and everything looks pretty square.  The thing I did wrong as it
    turned out was that I had a little padding on the top, making my CTView to
    slightly shift down, and therefore making the final string disappear.

    My code looks like this now:

    CGSize frameSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter,

    CFRangeMake(0, 0),

                                                                        NULL,

    CGSizeMake(self.bounds.size.width,
    CGFLOAT_MAX), &fitRange);

    self.frame = CGRectMake(self.frame.origin.x,

                                self.frame.origin.y,

                                self.frame.size.width,

                                frameSize.height + PADDING_TOP);  <<==correction for padding

    Cheers,

    - Koen.

    On Fri, May 17, 2013 at 5:42 PM, Quincey Morris <
    <quinceymorris...> wrote:

    > On May 17, 2013, at 11:20 , Robert Vojta <robert...> wrote:
    >
    > Height was wrong even if returned width <= constrained width.
    >
    >
    > Once I took proper account of the returned width, I got no more "wrong"
    > heights.
    >
    > However, IIRC, there was a separate issue that fractional line heights
    > didn't work correctly. Maybe you ran into that problem as well.
    >
    >
  • Yep, I saw it as well. But IIRC it was in rare occasions. Rare in my point of view, because we do use "normal" alphabet. the problem was in special characters, emoji, symbols, ... In these times I tried every possible combination/available method/property/calculation/... without success. Also tried 3rd party labels, later I tried DTCoreText from Cocoanetics with the same output - wrong height. I also asked Apple engineers and then I gave up. Not sure, but I did suspect them that they use private API or dunno what, because their apps didn't have problems.

    I'm pretty happy with attributed string and auto layout for now. It's enough for almost all needs and I'm glad that I'm not forced to dive into this "mess" again.

    On Friday, 17. May 2013 at 23:42, Quincey Morris wrote:

    > On May 17, 2013, at 11:20 , Robert Vojta <robert...> (mailto:<robert...>)> wrote:
    >
    >> Height was wrong even if returned width <= constrained width.
    > Once I took proper account of the returned width, I got no more "wrong" heights.
    >
    > However, IIRC, there was a separate issue that fractional line heights didn't work correctly. Maybe you ran into that problem as well.
    >
  • On 17/05/2013, at 11:38 PM, Koen van der Drift <koenvanderdrift...> wrote:

    > // calculate the size it occupies
    >
    > CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attrString);
    > []

    > // draw the string:
    >
    > []

    > CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef) attrString);

    Don't create the framesetter object twice. I'm not sure it has a bearing on your issue, but it would make sense that the framesetter used to calculate the frame should be the exact same one that draws the frame. It might be that creating it and calculating the size performs some caching that speeds up the second use. In any case it will use less memory if you only use one.

    --Graham
previous month may 2013 next month
MTWTFSS
    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31    
Go to today