NSAttributedString height limited by width and numberOfLines
Asked Answered
B

1

0

I need to calculate text rect in my custom label not using UILabel's sizeThatFits method. Code below not working correctly. The main idea is find CTLine at index = numberOfLines - 1 and return its max y position. But as a result text height sometimes too large and sometimes not enough to draw last line.

- (CGSize)fittingSizeWithSize:(CGSize)size numberOfLines:(NSInteger)numberOfLines {
    if (numberOfLines == 0) {
        return [self fittingSizeWithSize:size];
    }

    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)self);

    if (framesetter == NULL) {
        return CGSizeZero;
    }

    CGPathRef path = CGPathCreateWithRect(CGRectMake(0,0,size.width,size.height), NULL);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, self.length), path, NULL);

    NSArray *lines = (NSArray *) CTFrameGetLines(frame);
    if (lines.count == 0) {
        return CGSizeZero;
    }
    NSUInteger lineIndex = MIN((NSUInteger)numberOfLines, lines.count) - 1;
    CTLineRef line = (__bridge CTLineRef) lines[lineIndex];

    CGPoint origins[[lines count]];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);

    CGAffineTransform transform = CGAffineTransformMakeTranslation(0, size.height);
    transform = CGAffineTransformScale(transform, 1, -1);
    CGRect lineRect;
    CGFloat ascent;
    CGFloat descent;
    lineRect.size.width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, NULL); //8
    lineRect.size.height = ascent + descent;
    lineRect.origin.y = CGPointApplyAffineTransform(origins[lineIndex], transform).y;
    CGFloat height = CGRectGetMaxY(lineRect);

    CFRelease(path);
    CFRelease(framesetter);

    return CGSizeMake(size.width, height);
}

This category of NSAttributedString used in my UILabel subclass

@implementation SMBDLabel

- (void)drawTextInRect:(CGRect)rect {
    if (self.attributedText) {
        CGContextRef ctx = UIGraphicsGetCurrentContext();
        [self.attributedText drawInContext:ctx viewBounds:rect];
    } else {
        [super drawTextInRect:rect];
    }
}

- (CGRect)textRectForBounds:(CGRect)bounds limitedToNumberOfLines:(NSInteger)numberOfLines {
    CGSize size = [self.attributedText fittingSizeWithSize:bounds.size numberOfLines:numberOfLines];
    return CGRectMake(0, 0, size.width, size.height);
}

- (CGSize)sizeThatFits:(CGSize)size {
    return [self.attributedText fittingSizeWithSize:size numberOfLines:self.numberOfLines];
}

@end

I have no idea where my mistake. Maybe mistake actually in UILabel subclass

Bangkok answered 11/2, 2017 at 2:18 Comment(4)
Why use core text, when NSAttributedString already has boundingRectWithSize:options:context:?Worktable
As well as I can't use NSTextAttachment's(there are bug with line wrapping and no usable workaround) I drawing my custom image attachments. There are problem with replacing UILabel's with subclasses of UIView too.Bangkok
I'm not following any of this. You have Text Kit now. You shouldn't need Core Text for anything. And UITextView lets you use Text Kit directly, plus it can simulate the behavior of UILabel.Worktable
As I pointed, I can't use UILabel's drawing api and NSTextAttachment's have some bugs in text layout after images. All what I need is to calculate text height correctlyBangkok
B
1

Solution appears to be simplest. There are no need to get line origins and typographic bound. CTFramesetterSuggestFrameSizeWithConstraints with text specific text range will do all work

- (CGSize)fittingSizeWithSize:(CGSize)size numberOfLines:(NSInteger)numberOfLines {
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)self);

    if (!framesetter) {
        return CGSizeZero;
    }

    if (numberOfLines == 0) {
        CGSize textSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0,0), NULL, size, NULL);

        if (framesetter != NULL) {
            CFRelease(framesetter);
        }

        return CGSizeMake(ceilf(textSize.width), ceilf(textSize.height));
    } else {
        CGPathRef path = CGPathCreateWithRect(CGRectMake(0, 0, size.width, CGFLOAT_MAX), NULL);
        CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, self.length), path, NULL);
        if (path != NULL) {
            CFRelease(path);
        }


        NSArray *lines = (NSArray *)CTFrameGetLines(frame);
        __block CFIndex len = 0;

        [lines enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            if (numberOfLines > 0 && idx == numberOfLines) {
                *stop = YES;
                return;
            }

            CTLineRef line = (__bridge CTLineRef)obj;
            CFRange range = CTLineGetStringRange(line);

            len += range.length;
        }];

        CFRange strRange = CFRangeMake(0, len);
        CGSize textSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, strRange, NULL, size, NULL);

        if (framesetter != NULL) {
            CFRelease(framesetter);
        }

        return CGSizeMake(ceilf(textSize.width), ceilf(textSize.height));
    }
}
Bangkok answered 14/2, 2017 at 0:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.