Current non-UI threaded text height methods on the market are more or less problematic. In this paper, we reverse and analyze the sizeThatFits method of UILabel to get a simplified method of text height calculation. The method can run on any thread, so it can be used effectively in scenarios where height is computed asynchronously or where pre-sizing is required.

As you can see from the official iOS implementation, text height takes into account simple text strings, attribute strings, font size, maximum line numberOfLines, paragraph information, paragraph alignment, hypeation, paragraph header indentation, shadow offset, and more. Here is the concrete implementation code:

/// When using this method, please indicate the source: Ouyang Eldest Brother 2013. This method conforms to the MIT protocol specification. /// Github address: / / https://github.com/youngsoft/simple text or attribute string adaptive size / / / @ param fitsSize specify the size of the restrictions, refer to the UILabel sizeThatFits the meaning of the parameters. // @param text specifies the maximum numberOfLines to display. // @param font specifies the font of the computed text, // @param textAlignment specifies textAlignment. The default is NSTextAlignmentNatural. // @param lineBreakMode To specify multiple rows hyphenation mode, the default mode can use the default hyphenation UILabel NSLineBreakByTruncatingTail / / / @ param minimumScaleFactor specified text, the minimum zoom factor, fill in zero by default. This parameter is used in scenarios where the font size can be automatically reduced to fit the display. /// @param shadowOffset specifies the shadowOffset position. Note that this offset position is valid only when both the shadow color and the shadowOffset position are specified. Pass CGSizeZero if shadows are not considered, otherwise shadows will participate in the sizing calculation. / / / @returnCGSize calcTextSize(CGSize fitsSize, ID text, NSInteger numberOfLines, UIFont *font, NSTextAlignment textAlignment, NSLineBreakMode lineBreakMode, CGFloat minimumScaleFactor, CGSize shadowOffset) {if (text == nil || [text length] <= 0) {
        returnCGSizeZero; } NSAttributedString *calcAttributedString = nil; // Use the default font if no font is specified.if(font == nil) { font = [UIFont systemFontOfSize:17]; } CGFloat systemVersion = [UIDevice currentDevice].systemVersion.floatValue; NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; paragraphStyle.alignment = textAlignment; paragraphStyle.lineBreakMode = lineBreakMode; // Set the line hyphenation policy only when the system is greater than or equal to 11.if(systemVersion >= 11.0) {@try {[paragraphStylesetValue:@(1) forKey:@"lineBreakStrategy"];
        } @catch (NSException *exception) {}
    }
        
    if ([text isKindOfClass:NSString.class]) {
        calcAttributedString = [[NSAttributedString alloc] initWithString:(NSString *)text attributes:@{NSFontAttributeName:font, NSParagraphStyleAttributeName:paragraphStyle}];
    } else{ NSAttributedString *originAttributedString = (NSAttributedString *)text; // Always add default font and paragraph information for attribute strings. NSMutableAttributedString *mutableCalcAttributedString = [[NSMutableAttributedString alloc] initWithString:originAttributedString.string attributes:@{NSFontAttributeName:font, NSParagraphStyleAttributeName:paragraphStyle}]; // Add the original attributes. [originAttributedString enumerateAttributesInRange:NSMakeRange(0, originAttributedString.string.length) options:0 usingBlock:^(NSDictionary<NSAttributedStringKey,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) { [mutableCalcAttributedString addAttributes:attrs range:range]; }]; // Take paragraph information again, because it is possible that the attribute string already contains paragraph information.if(systemVersion > = 11.0) {NSParagraphStyle * alternativeParagraphStyle = [mutableCalcAttributedString attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:NULL];if(alternativeParagraphStyle ! = nil) { paragraphStyle = (NSMutableParagraphStyle*)alternativeParagraphStyle; } } calcAttributedString = mutableCalcAttributedString; } // Adjust the value of fitsSize, where the width is adjusted to be unlimited as long as the width is less than or equal to 0 or the display line, and the height is always unlimited. fitsSize.height = FLT_MAX;if(fitsSize.width <= 0 || numberOfLines == 1) { fitsSize.width = FLT_MAX; NSStringDrawingContext *context = [[NSStringDrawingContext alloc] init]; context.minimumScaleFactor = minimumScaleFactor; @try {// Since the following attributes are not public, we implement them KVC way. [contextsetValue:@(numberOfLines) forKey:@"maximumNumberOfLines"];
        if(numberOfLines ! = 1) { [contextsetValue:@(YES) forKey:@"wrapsForTruncationMode"];
        }
        [context setValue:@(YES) forKey:@"wantsNumberOfLineFragments"]; } @catch (NSException *exception) {} // Calculate the bounds value of the property string. CGRect rect = [calcAttributedString boundingRectWithSize:fitsSize options:NSStringDrawingUsesLineFragmentOrigin context:context]; // Special indentation is required for the first line of the paragraph! // If there is only one line, add the value of the first indent directly, otherwise special processing is performed. CGFloat firstLineHeadIndent = paragraphStyle.firstLineHeadIndent;if(firstLineHeadIndent ! NSInteger numberOfDrawingLines = [[context valueForKey:@]"numberOfLineFragments"] integerValue];
        if (numberOfDrawingLines == 1) {
            rect.size.width += firstLineHeadIndent;
        } else{// Take the number of lines of content. NSString *string = calcAttributedString.string; NSCharacterSet *charset = [NSCharacterSet newlineCharacterSet]; NSArray *lines = [string componentsSeparatedByCharactersInSet:charset]; NSString *lastLine = lines.lastObject; NSString *lastLine = lines.lastObject; NSInteger numberOfContentLines = lines.count - (NSInteger)(lastLine.length == 0); // The number of valid lines of content is subtracted from the last blank line.if (numberOfLines == 0) {
                numberOfLines = NSIntegerMax;
            }
            if(numberOfLines > numberOfContentLines) numberOfLines = numberOfContentLines; // Indent the first line only if the number of lines drawn is equal to the specified number! This code is implemented according to disassembly, but does not understand why equality is set?if(numberOfDrawingLines == numberOfLines) { rect.size.width += firstLineHeadIndent; }}} // Take the minimum width values in fitsSize and rect.if(rect.size.width > fitsSize.width) { rect.size.width = fitsSize.width; Rect.size. width += fabs(shadowoffset.width); rect.size.height += fabs(shadowOffset.height); Here, multiply the original logical point by the scale to get the physical pixel, then round it, and then divide by the scale to get the logical point that can be effectively displayed. CGFloat scale = [UIScreen mainScreen].scale; rect.size.width = ceil(rect.size.width * scale) / scale; rect.size.height = ceil(rect.size.height *scale) / scale;returnrect.size; } // A compact version of the above method NS_INLINE CGSize calcTextSizeV2(CGSize fitsSize, ID text, NSInteger numberOfLines, UIFont *font) {returnCalcTextSize (numberOfLines fitsSize, text, the font, NSTextAlignmentNatural NSLineBreakByTruncatingTail, 0.0, CGSizeZero); }Copy the code

Here are the specific validation test cases (the use cases passed on iOS9 to iOS13) :

   CFTimeInterval simpleTextUILabelInterval = 0;
    CFTimeInterval simpleTextNOUILabelInterval = 0;
    CFTimeInterval attributedTextUILabelInterval = 0;
    CFTimeInterval attributedTextNOUILabelInterval = 0;
    NSArray *testStringArray = @[@"You"The @"Good"The @"In"The @"The"The @"w"The @"i"The @"d"The @"t"The @"h"The @","The @"。"The @"a"The @"b"The @"c"The @"\n"The @"1"The @"5"The @"2"The @"j"The @"A"The @"J"The @"0"The @"🆚"The @"👃"The @""];
    srand(time(NULL));
    for(int i = 0; i < 5000; I++) {// randomly generate 0 to 100 characters. int textLength = rand() % 100; NSMutableString *text = [NSMutableString new];for (int j = 0; j < textLength; j++) {
            [text appendString:testStringArray[rand()%testStringArray.count]];
        }
        if (text.length == 0)
            continue; CGSize fitSize = CGSizeMake(rand()%1000, rand()%1000); // Test simple text. UILabel *label = [UILabel new]; label.text = text; label.numberOfLines = rand() % 100; label.textAlignment = rand() % 5; label.lineBreakMode = rand() % 7; Label.font = [UIFont systemFontOfSize:rand()%30 + 5.0]; CFTimeInterval start = CACurrentMediaTime(); CGSize sz1 = [label sizeThatFits:fitSize]; simpleTextUILabelInterval += CACurrentMediaTime() - start; start = CACurrentMediaTime(); CGSize sz2 = calcTextSize(fitSize, label.text, label.numberOfLines, label.font, label.textAlignment, label.lineBreakMode, label.minimumScaleFactor, CGSizeZero); simpleTextNOUILabelInterval += CACurrentMediaTime() - start; NSAssert(CGSizeEqualToSize(sz1, sz2), @""); // Test rich text NSRange range1 = NSMakeRange(0, rand()%text.length); NSMutableParagraphStyle *paragraphStyle1 = [[NSMutableParagraphStyle alloc] init]; paragraphStyle1.lineSpacing = rand() % 20; paragraphStyle1.firstLineHeadIndent = rand() %10; paragraphStyle1.paragraphSpacing = rand() % 30; paragraphStyle1.headIndent = rand() % 10; paragraphStyle1.tailIndent = rand() % 10; UIFont *font1 = [UIFont systemFontOfSize:rand() % 20 + 3.0]; NSRange range2 = NSMakeRange(range1.length, text.length - range1.length); NSMutableParagraphStyle *paragraphStyle2 = [[NSMutableParagraphStyle alloc] init]; paragraphStyle2.lineSpacing = rand() % 20; paragraphStyle2.firstLineHeadIndent = rand() %10; paragraphStyle2.paragraphSpacing = rand() % 30; paragraphStyle2.headIndent = rand() % 10; paragraphStyle2.tailIndent = rand() % 10; UIFont *font2 = [UIFont systemFontOfSize:rand() % 20 + 3.0] NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:text]; [attributedText addAttributes:@{NSParagraphStyleAttributeName:paragraphStyle1,NSFontAttributeName:font1} range:range1]; [attributedText addAttributes:@{NSParagraphStyleAttributeName:paragraphStyle2,NSFontAttributeName:font2} range:range2]; label = [UILabel new]; label.numberOfLines = rand() % 100; label.textAlignment = rand() % 5; label.lineBreakMode = rand() % 7; Label.font = [UIFont systemFontOfSize:rand()%30 + 5.0]; label.attributedText = attributedText; start = CACurrentMediaTime(); CGSize sz3 = [label sizeThatFits:fitSize]; attributedTextUILabelInterval += CACurrentMediaTime() - start; start = CACurrentMediaTime(); CGSize sz4 = calcTextSize(fitSize, label.attributedText, label.numberOfLines, label.font, label.textAlignment, Label. LineBreakMode, 0.0 CGSizeZero); attributedTextNOUILabelInterval += CACurrentMediaTime() - start; NSAssert(CGSizeEqualToSize(sz3, sz4), @"");
    }
    
    simpleTextUILabelInterval *= 1000;
    simpleTextNOUILabelInterval *= 1000;
    attributedTextUILabelInterval *= 1000;
    attributedTextNOUILabelInterval *= 1000;
    NSLog(@"Total time (ms) for simple text calculation of UILabel :%.3f, average time :%.3f",simpleTextUILabelInterval, simpleTextUILabelInterval / 5000);
    NSLog(@"Total non-Uilabel time for simple text computation (ms):%.3f, average time :%.3f",simpleTextNOUILabelInterval, simpleTextNOUILabelInterval / 5000);
    NSLog(@"Total rich-text computing UILabel time (ms):%.3f, average time :%.3f",attributedTextUILabelInterval, attributedTextUILabelInterval / 5000);
    NSLog(@"Total non-uilabel rich-text computing time (ms):%.3f, average time :%.3f",attributedTextNOUILabelInterval, attributedTextNOUILabelInterval / 5000);
        
Copy the code

Attention: ouyang eldest brother 2013 Jane ouyang book 2013 Denver | | ouyang eldest brother eldest brother 2013 dead simple