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