From b2188ae51f19974e123dd0523069cce005625773 Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Sat, 17 Oct 2026 01:09:22 +0200 Subject: [PATCH] Avoid copying text fragments and re-attributing runs when building NSAttributedString Iterate AttributedString fragments by reference instead of copying each Fragment, and build each fragment's NSAttributedString with all of its attributes at once, including the event emitter, for text and attachment fragments alike. This removes the second addAttributes:range: pass, the wrapper function that ran it, and the mutableCopy of the attachment string, which is now created directly with initWithString:attributes:. Co-Authored-By: Claude Opus 5.5 --- .../RCTAttributedTextUtils.mm | 34 +++++++------------ 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm index 95ff5a0d180e..b988908b6fd6 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm @@ -405,6 +405,9 @@ void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText) const AttributedString::Fragment &fragment, UIImage *placeholderImage) { + NSString *string; + NSMutableDictionary *attributes; + if (fragment.isAttachment()) { auto layoutMetrics = fragment.parentShadowView.layoutMetrics; CGRect bounds = { @@ -415,41 +418,28 @@ void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText) attachment.image = placeholderImage; attachment.bounds = bounds; - return [[NSMutableAttributedString attributedStringWithAttachment:attachment] mutableCopy]; + string = [NSString stringWithFormat:@"%C", (unichar)NSAttachmentCharacter]; + attributes = [NSMutableDictionary dictionaryWithObject:attachment forKey:NSAttachmentAttributeName]; } else { NSString *decoded = [[NSString alloc] initWithBytes:fragment.string.data() length:fragment.string.size() encoding:NSUTF8StringEncoding]; - NSString *string = decoded != nil ? decoded : @""; + string = decoded != nil ? decoded : @""; if (fragment.textAttributes.textTransform.has_value()) { auto textTransform = fragment.textAttributes.textTransform.value(); string = RCTNSStringFromStringApplyingTextTransform(string, textTransform); } - return [[NSMutableAttributedString alloc] - initWithString:string - attributes:RCTNSTextAttributesFromTextAttributes(fragment.textAttributes)]; + attributes = RCTNSTextAttributesFromTextAttributes(fragment.textAttributes); } -} - -static NSMutableAttributedString *RCTNSAttributedStringFragmentWithAttributesFromFragment( - const AttributedString::Fragment &fragment, - UIImage *placeholderImage) -{ - auto nsAttributedStringFragment = RCTNSAttributedStringFragmentFromFragment(fragment, placeholderImage); + // Set with the initial attributes; adding it afterwards would re-split the runs. if (fragment.parentShadowView.componentHandle) { - auto eventEmitterWrapper = RCTWrapEventEmitter(fragment.parentShadowView.eventEmitter); - - NSDictionary *additionalTextAttributes = - @{RCTAttributedStringEventEmitterKey : eventEmitterWrapper}; - - [nsAttributedStringFragment addAttributes:additionalTextAttributes - range:NSMakeRange(0, nsAttributedStringFragment.length)]; + attributes[RCTAttributedStringEventEmitterKey] = RCTWrapEventEmitter(fragment.parentShadowView.eventEmitter); } - return nsAttributedStringFragment; + return [[NSMutableAttributedString alloc] initWithString:string attributes:attributes]; } NSAttributedString *RCTNSAttributedStringFromAttributedString(const AttributedString &attributedString) @@ -464,9 +454,9 @@ void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText) [nsAttributedString beginEditing]; - for (auto fragment : attributedString.getFragments()) { + for (const auto &fragment : attributedString.getFragments()) { NSMutableAttributedString *nsAttributedStringFragment = - RCTNSAttributedStringFragmentWithAttributesFromFragment(fragment, placeholderImage); + RCTNSAttributedStringFragmentFromFragment(fragment, placeholderImage); [nsAttributedString appendAttributedString:nsAttributedStringFragment]; }