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

4
  • Are you interested in just iOS7 or support for older OSes as well?
    – Léo Natan
    Oct 19, 2013 at 1:48
  • 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?
    – Anurag
    Oct 19, 2013 at 8:41
  • Yes, your entire code would be simplified greatly, including calculating the height.
    – Léo Natan
    Oct 19, 2013 at 13:20
  • No solution for previous iOS. Thanks to Leo for iOS7 solution
    – dotAramis
    Oct 21, 2013 at 10:47

1 Answer 1

10
+200

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.

6
  • But i guess there is no such one
    – dotAramis
    Oct 21, 2013 at 10:45
  • 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.
    – Léo Natan
    Oct 21, 2013 at 12:18
  • 1
    Thanks for the answer Leo.. This is great.. I'm using a class cluster as you suggested to have two implementations and it works great
    – Anurag
    Oct 26, 2013 at 2:28
  • @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.
    – Unheilig
    Mar 14, 2014 at 20:18
  • @Unheilig 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): Tutorial
    – Léo Natan
    Mar 14, 2014 at 20:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.