From 77cdaa8733db7a7de25b8b0aef87a11f91be9efd Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 17 Mar 2025 00:14:29 -0700 Subject: [PATCH] Remove the workaround used for measuring multiline text on iOS (#50028) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/50028 Changelog: [IOS][CHANGED] - Replace a workaround for measuring multiline text with `maximumNumberOfLines` on iOS with a proper solution ## Summary: In https://github.com/facebook/react-native/pull/49549 support for `numberOfLines` was added to iOS `TextInput`. Along that, a workaround for measuring multiline text with limited number of lines was added to avoid an edge case coming from `NSTextContainer` measuring empty lines even if they are over the line limit. This PR handles that case properly by counting and measuring individual lines to properly handle the offending edge case. Reviewed By: NickGerleman Differential Revision: D71111841 fbshipit-source-id: 6adb4450a13fcc845604622ea76576658a7537aa --- .../textlayoutmanager/RCTTextLayoutManager.mm | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm index 338641477c5..781143a384a 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm @@ -47,24 +47,11 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi } CGSize maximumSize = CGSize{layoutConstraints.maximumSize.width, CGFLOAT_MAX}; - - // A workaround for the issue with empty line measurement: - // When maximumNumberOfLines is set to N and N+1 line is empty, the returned - // measurement is for N+1 lines. Adding any character to that line results - // in the correct measurement. - if (paragraphAttributes.maximumNumberOfLines > 0 && attributedString.length > 0 && - [[attributedString string] characterAtIndex:attributedString.length - 1] == '\n') { - NSMutableAttributedString *mutableString = - [[NSMutableAttributedString alloc] initWithAttributedString:attributedString]; - [mutableString replaceCharactersInRange:NSMakeRange(attributedString.length - 1, 1) withString:@"\n "]; - attributedString = mutableString; - } - NSTextStorage *textStorage = [self _textStorageAndLayoutManagerWithAttributesString:attributedString paragraphAttributes:paragraphAttributes size:maximumSize]; - return [self _measureTextStorage:textStorage]; + return [self _measureTextStorage:textStorage paragraphAttributes:paragraphAttributes]; } - (TextMeasurement)measureAttributedString:(AttributedString)attributedString @@ -341,6 +328,7 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi } - (TextMeasurement)_measureTextStorage:(NSTextStorage *)textStorage + paragraphAttributes:(ParagraphAttributes)paragraphAttributes { NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; NSTextContainer *textContainer = layoutManager.textContainers.firstObject; @@ -348,6 +336,8 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; __block BOOL textDidWrap = NO; + __block NSUInteger linesEnumerated = 0; + __block CGFloat enumeratedLinesHeight = 0; [layoutManager enumerateLineFragmentsForGlyphRange:glyphRange usingBlock:^( @@ -363,6 +353,13 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi [textStorage.string characterAtIndex:lastCharacterIndex] == '\n'; if (!endsWithNewLine && textStorage.string.length > lastCharacterIndex + 1) { textDidWrap = YES; + } + if (linesEnumerated++ < paragraphAttributes.maximumNumberOfLines) { + enumeratedLinesHeight = usedRect.origin.y + usedRect.size.height; + } + if (textDidWrap && + (paragraphAttributes.maximumNumberOfLines == 0 || + linesEnumerated >= paragraphAttributes.maximumNumberOfLines)) { *stop = YES; } }]; @@ -373,6 +370,18 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi size.width = textContainer.size.width; } + if (paragraphAttributes.maximumNumberOfLines != 0) { + // If maximumNumberOfLines is set, we cannot rely on setting it on the NSTextContainer + // due to an edge case where it returns wrong height: + // When maximumNumberOfLines is set to N and the N+1 line is empty, the measured height + // is N+1 lines (incorrect). Adding any characted to the N+1 line, making it non-empty + // casuses the measured height to be N lines (correct). + if (linesEnumerated < paragraphAttributes.maximumNumberOfLines) { + enumeratedLinesHeight += layoutManager.extraLineFragmentUsedRect.size.height; + } + size.height = enumeratedLinesHeight; + } + size = (CGSize){RCTCeilPixelValue(size.width), RCTCeilPixelValue(size.height)}; __block auto attachments = TextMeasurement::Attachments{};