Arsynth
Arsynth

Reputation: 793

NSAttributedString height limited by width and numberOfLines

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

Upvotes: 0

Views: 951

Answers (1)

Arsynth
Arsynth

Reputation: 793

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

Upvotes: 1

Related Questions