CTFramesetterSuggestFrameSizeWithConstraints with clipping attributes returning zero height
Asked Answered
J

1

3

H! I'm writing a textview with subviews clipping features. The idea is to make all text draw around all subviews. The problem is to get it's content height.

Due to the lack of documentation, I decided that the attributes dictionary for CTFramesetterSuggestFrameSizeWithConstraints is the same as for the CTFramesetterCreateFrame.

Here is my clipping paths code:

-(CFDictionaryRef)clippingPathsDictionary{
if(self.subviews.count==0)return NULL;

NSMutableArray *pathsArray = [[NSMutableArray alloc] init];

CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformScale(transform, 1, -1);
transform = CGAffineTransformTranslate(transform, 0, -self.bounds.size.height);

for (int i=0; i<self.subviews.count; i++) {
    UIView *clippingView = self.subviews[i];
    CGPathRef clipPath = CGPathCreateWithRect(clippingView.frame, &transform);

    NSDictionary *clippingPathDictionary = [NSDictionary dictionaryWithObject:(__bridge id)(clipPath) forKey:(__bridge NSString *)kCTFramePathClippingPathAttributeName];
    [pathsArray addObject:clippingPathDictionary];
    CFRelease(clipPath);
}


int eFrameWidth=0;
CFNumberRef frameWidth = CFNumberCreate(NULL, kCFNumberNSIntegerType, &eFrameWidth);

int eFillRule = kCTFramePathFillEvenOdd;
CFNumberRef fillRule = CFNumberCreate(NULL, kCFNumberNSIntegerType, &eFillRule);

int eProgression = kCTFrameProgressionTopToBottom;
CFNumberRef progression = CFNumberCreate(NULL, kCFNumberNSIntegerType, &eProgression);


CFStringRef keys[] = { kCTFrameClippingPathsAttributeName, kCTFramePathFillRuleAttributeName, kCTFrameProgressionAttributeName, kCTFramePathWidthAttributeName};
CFTypeRef values[] = { (__bridge CFTypeRef)(pathsArray), fillRule, progression, frameWidth};




CFDictionaryRef clippingPathsDictionary = CFDictionaryCreate(NULL,
                                                             (const void **)&keys, (const void **)&values,
                                                             sizeof(keys) / sizeof(keys[0]),
                                                             &kCFTypeDictionaryKeyCallBacks,
                                                             &kCFTypeDictionaryValueCallBacks);
return clippingPathsDictionary;}

I use it to draw text and it works ok. Here is my drawing code:

- (void)drawRect:(CGRect)rect{
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformScale(transform, 1, -1);
transform = CGAffineTransformTranslate(transform, 0, -rect.size.height);


CGContextRef context = UIGraphicsGetCurrentContext();
CGContextConcatCTM(context, transform);

CFAttributedStringRef attributedString = (__bridge CFAttributedStringRef)self.attributedString;
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString(attributedString);

CFDictionaryRef  attributesDictionary = [self clippingPathsDictionary];
CGPathRef path = CGPathCreateWithRect(rect, &transform);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, self.attributedString.length), path, attributesDictionary);
CFRelease(path);
CFRelease(attributesDictionary);

CTFrameDraw(frame, context);
CFRelease(frameSetter);
CFRelease(frame);

CGSize contentSize = [self contentSizeForWidth:self.bounds.size.width];

CGPathRef highlightPath = CGPathCreateWithRect((CGRect){CGPointZero, contentSize}, &transform);
CGContextSetFillColorWithColor(context, [UIColor colorWithRed:.0 green:1 blue:.0 alpha:.3].CGColor);
CGContextAddPath(context, highlightPath);
CGContextDrawPath(context, kCGPathFill);
CFRelease(highlightPath);

}

That's the result:

Drawing result That is exactly what I was expecting!

Finally, here is the code to check the height:

-(CGSize)contentSizeForWidth:(float)width{
CFAttributedStringRef attributedString = (__bridge CFAttributedStringRef)self.attributedString;
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString(attributedString);

CFDictionaryRef attributesDictionary = [self clippingPathsDictionary];
CGSize size = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter, CFRangeMake(0, self.attributedString.length),  attributesDictionary, CGSizeMake(width, CGFLOAT_MAX), NULL);
NSLog(@"%s: size = %@",__PRETTY_FUNCTION__, NSStringFromCGSize(size));
CFRelease(attributesDictionary);
CFRelease(frameSetter);
return size;

}

Output looks like this:

Core Text Demo[2222:a0b] -[DATextView contentSizeForWidth:]: size = {729.71484, 0}

Does someone have any solution? Thanks for attention:)

Jaymie answered 15/10, 2013 at 9:15 Comment(4)
Are you interested in just iOS7 or support for older OSes as well?Chaschase
It would be nice to get an answer to the original question using the Core Text API. Do the new Text Kit APIs in iOS 7 have an easy way to calculate the height correctly?Bakst
Yes, your entire code would be simplified greatly, including calculating the height.Chaschase
No solution for previous iOS. Thanks to Leo for iOS7 solutionJaymie
T
10

This seems to me like an iOS7 bug. I have been tinkering around, and under iOS6, CTFramesetterSuggestFrameSizeWithConstraints returns a size with height larger than 0. Same code under iOS7 returns a height of 0.

CTFramesetterSuggestFrameSizeWithConstraints is known for its buggy and undocumented behavior. For example, your code under iOS6 returns an incorrect height due to CTFramesetterSuggestFrameSizeWithConstraints performing an incorrect calculation, and the last line is omitted from the calculation. Here is your code running under iOS6:

enter image description here

You should open a bug report for these issues, as well as request documentation enhancements, at: https://bugreport.apple.com

Under iOS7, with TextKit, you can achieve the same result much quicker and more elegantly:

@interface LeoView : UIView

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

@end

@implementation LeoView

-(void)awakeFromNib
{
    //Leo: This should not be done here. Just for example. You should do this in the init.

    self.textStorage = [[NSTextStorage alloc] initWithString:@"Lorem ipsum dolor [...]"];

    self.layoutManager = [[NSLayoutManager alloc] init];
    [self.textStorage addLayoutManager:self.layoutManager];

    self.textContainer = [[NSTextContainer alloc] initWithSize:self.bounds.size];
    [self.layoutManager addTextContainer:self.textContainer];
}

- (void)layoutSubviews
{
    [super layoutSubviews];

    //Leo: At this point the user may have set content inset, so need to take into account.
    CGSize size = self.bounds.size;
    size.width -= (self.contentInset.left + self.contentInset.right);
    size.height -= (self.contentInset.top + self.contentInset.bottom);

    self.textContainer.size = size;

    NSMutableArray* exclussionPaths = [NSMutableArray new];
    for (UIView* subview in self.subviews)
    {
        if(subview.isHidden)
            continue;

        CGRect frame = subview.frame;
        //Leo: If there is content inset, need to compensate.
        frame.origin.y -= self.contentInset.top;
        frame.origin.x -= self.contentInset.left;

        [exclussionPaths addObject:[UIBezierPath bezierPathWithRect:frame]];
    }
    self.textContainer.exclusionPaths = exclussionPaths;

    [self setNeedsDisplay];
}

- (void)drawRect:(CGRect)rect
{
    [self.layoutManager drawGlyphsForGlyphRange:[self.layoutManager glyphRangeForTextContainer:self.textContainer] atPoint:CGPointMake(self.contentInset.left, self.contentInset.top)];

    //Leo: Move used rectangle according to content inset.
    CGRect usedRect = [self.layoutManager usedRectForTextContainer:self.textContainer];
    usedRect.origin.x += self.contentInset.left;
    usedRect.origin.y += self.contentInset.top;

    CGAffineTransform transform = CGAffineTransformIdentity;
    transform = CGAffineTransformScale(transform, 1, -1);
    transform = CGAffineTransformTranslate(transform, 0, -rect.size.height);

    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextConcatCTM(context, transform);
    CGPathRef highlightPath = CGPathCreateWithRect(usedRect, &transform);
    CGContextSetFillColorWithColor(context, [UIColor colorWithRed:.0 green:1 blue:.0 alpha:.3].CGColor);
    CGContextAddPath(context, highlightPath);
    CGContextDrawPath(context, kCGPathFill);
    CFRelease(highlightPath);
}

@end

And here is the result with contentInset set to UIEdgeInsetsMake(20, 200, 0, 35):

enter image description here

I used your code for drawing the green rectangle around the text.

Torpedo answered 19/10, 2013 at 22:42 Comment(6)
But i guess there is no such oneJaymie
Well, you could create two implementations of the view, for each OS. Your code works somewhat on iOS6. There are some things you can do to fix the missing last line. Call it "legacy implementation". And have the iOS7 code as "modern implementation". You can even use class clustering to decide in runtime which implementation to use.Chaschase
Thanks for the answer Leo.. This is great.. I'm using a class cluster as you suggested to have two implementations and it works greatBakst
@LeoNatan Hi, I was hoping if you could be so kind as to cite me a reference/reading/post on "class clustering to decide in runtime which implementation to use."? I never heard of it, and would like to learn more about it. Thanks very much in advance.Choreographer
@Choreographer Class cluster is a design pattern used a lot in Cocoa and Cocoa Touch. Here is some reading materials: Doc 1 Doc 2 and the most important (demonstrates exactly what you need): TutorialChaschase
@LeoNatan Thank you! I will definitely go do some reading up.Choreographer

© 2022 - 2024 — McMap. All rights reserved.