Skip to content

Commit 7cb42ee

Browse files
committed
fix(ios): CoreText layout exception may occur in nested text (Tencent#4450)
implement pending attributes collection for nested text relayout
1 parent 112bfcc commit 7cb42ee

File tree

1 file changed

+51
-7
lines changed

1 file changed

+51
-7
lines changed

renderer/native/ios/renderer/component/text/HippyShadowText.mm

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@
4646
// Distance to the bottom of the baseline, for text attachment baseline layout, NSNumber value
4747
NSAttributedStringKey const HippyVerticalAlignBaselineOffsetAttributeName = @"HippyVerticalAlignBaselineOffsetAttributeName";
4848

49+
// Keys for pending attributes collection (avoid string hardcode)
50+
static NSString *const HippyPendingRangeKey = @"HippyPendingRangeKey";
51+
static NSString *const HippyPendingOffsetKey = @"HippyPendingOffsetKey";
52+
static NSString *const HippyPendingValueKey = @"HippyPendingValueKey";
53+
4954

5055
CGFloat const HippyTextAutoSizeWidthErrorMargin = 0.05;
5156
CGFloat const HippyTextAutoSizeHeightErrorMargin = 0.025;
@@ -91,6 +96,9 @@ @interface HippyShadowText () <NSLayoutManagerDelegate>
9196
BOOL _isNestedText; // Indicates whether Text is nested, for speeding up typesetting calculations
9297
BOOL _needRelayoutText; // special styles require two layouts, eg. verticalAlign etc
9398
hippy::LayoutMeasureMode _cachedTextStorageWidthMode; // cached width mode when building text storage
99+
// Collect pending edits to avoid mutating textStorage during layout callbacks
100+
NSMutableArray<NSDictionary *> *_pendingBaselineOffsets; // entries: { range:NSValue(NSRange), offset:NSNumber }
101+
NSMutableArray<NSDictionary *> *_pendingAttachmentBaselineBottoms; // entries: { range:NSValue(NSRange), value:NSNumber }
94102
}
95103

96104
@end
@@ -171,6 +179,8 @@ - (instancetype)init {
171179
if (NSWritingDirectionRightToLeft == [[HippyI18nUtils sharedInstance] writingDirectionForCurrentAppLanguage]) {
172180
self.textAlign = NSTextAlignmentRight;
173181
}
182+
_pendingBaselineOffsets = [NSMutableArray array];
183+
_pendingAttachmentBaselineBottoms = [NSMutableArray array];
174184
}
175185
return self;
176186
}
@@ -391,15 +401,49 @@ - (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width widthMode:(hippy::Lay
391401

392402
layoutManager.delegate = self;
393403
[layoutManager addTextContainer:textContainer];
404+
// start clean collection for this layout build
405+
[_pendingBaselineOffsets removeAllObjects];
406+
[_pendingAttachmentBaselineBottoms removeAllObjects];
394407
[layoutManager ensureLayoutForTextContainer:textContainer];
408+
409+
// Apply pending attributes collected during the first layout pass.
410+
if (_pendingBaselineOffsets.count > 0 || _pendingAttachmentBaselineBottoms.count > 0) {
411+
[textStorage beginEditing];
412+
NSUInteger const storageLength = textStorage.length;
413+
for (NSDictionary *entry in _pendingBaselineOffsets) {
414+
NSValue *rangeValue = entry[HippyPendingRangeKey];
415+
NSNumber *offsetValue = entry[HippyPendingOffsetKey];
416+
if (!rangeValue || !offsetValue) { continue; }
417+
NSRange r = rangeValue.rangeValue;
418+
if (r.location == NSNotFound || r.length == 0) { continue; }
419+
if (NSMaxRange(r) > storageLength) { continue; }
420+
[textStorage addAttribute:NSBaselineOffsetAttributeName value:offsetValue range:r];
421+
}
422+
for (NSDictionary *entry in _pendingAttachmentBaselineBottoms) {
423+
NSValue *rangeValue = entry[HippyPendingRangeKey];
424+
NSNumber *value = entry[HippyPendingValueKey];
425+
if (!rangeValue || !value) { continue; }
426+
NSRange r = rangeValue.rangeValue;
427+
if (r.location == NSNotFound || r.length == 0) { continue; }
428+
if (NSMaxRange(r) > storageLength) { continue; }
429+
[textStorage addAttribute:HippyVerticalAlignBaselineOffsetAttributeName value:value range:r];
430+
}
431+
[textStorage endEditing];
432+
[_pendingBaselineOffsets removeAllObjects];
433+
[_pendingAttachmentBaselineBottoms removeAllObjects];
434+
_needRelayoutText = YES;
435+
}
395436

396437
// for better perf, only do relayout when MeasureMode is MeasureModeExactly
397438
if (_needRelayoutText && hippy::LayoutMeasureMode::Exactly == widthMode) {
398-
// relayout text
439+
// relayout text after applying pending attributes from first pass
399440
[layoutManager invalidateLayoutForCharacterRange:NSMakeRange(0, textStorage.length) actualCharacterRange:nil];
400441
[layoutManager removeTextContainerAtIndex:0];
401442
[layoutManager addTextContainer:textContainer];
402443
[layoutManager ensureLayoutForTextContainer:textContainer];
444+
// clear any collections from the relayout pass
445+
[_pendingBaselineOffsets removeAllObjects];
446+
[_pendingAttachmentBaselineBottoms removeAllObjects];
403447
_needRelayoutText = NO;
404448
}
405449

@@ -1098,9 +1142,9 @@ - (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldSetLineFragmentRect
10981142
CGFloat maxTotalHeight = MAX((maxAttachmentHeight + textBaselineToBottom), maxFont.lineHeight);
10991143
realBaselineOffset = (CGRectGetHeight(*lineFragmentUsedRect) - maxTotalHeight) / 2.f;
11001144
if (hasAttachment) {
1101-
[textStorage addAttribute:HippyVerticalAlignBaselineOffsetAttributeName
1102-
value:@(realBaselineOffset + textBaselineToBottom)
1103-
range:storageRange];
1145+
// Defer writing attribute to avoid mutating storage during layout
1146+
[_pendingAttachmentBaselineBottoms addObject:@{ HippyPendingRangeKey: [NSValue valueWithRange:storageRange],
1147+
HippyPendingValueKey: @(realBaselineOffset + textBaselineToBottom) }];
11041148
}
11051149
}
11061150

@@ -1142,9 +1186,9 @@ - (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldSetLineFragmentRect
11421186
break;
11431187
}
11441188
if (abs(offset) > .0f && !attrs[HippyShadowViewAttributeName]) {
1145-
// only set for Text
1146-
[textStorage addAttribute:NSBaselineOffsetAttributeName value:@(offset) range:range];
1147-
_needRelayoutText = YES;
1189+
// only set for Text; defer to avoid mutation during layout
1190+
[_pendingBaselineOffsets addObject:@{ HippyPendingRangeKey: [NSValue valueWithRange:range],
1191+
HippyPendingOffsetKey: @(offset) }];
11481192
}
11491193
}
11501194
}];

0 commit comments

Comments
 (0)