Skip to content

Commit 41ff6ab

Browse files
authored
fix(ios): CoreText layout exception may occur in nested text (Tencent#4450)
implement pending attributes collection for nested text relayout
1 parent 076ff03 commit 41ff6ab

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
@@ -172,6 +180,8 @@ - (instancetype)init {
172180
if (NSWritingDirectionRightToLeft == [[HippyI18nUtils sharedInstance] writingDirectionForCurrentAppLanguage]) {
173181
self.textAlign = NSTextAlignmentRight;
174182
}
183+
_pendingBaselineOffsets = [NSMutableArray array];
184+
_pendingAttachmentBaselineBottoms = [NSMutableArray array];
175185
}
176186
return self;
177187
}
@@ -392,15 +402,49 @@ - (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width widthMode:(hippy::Lay
392402

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

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

@@ -1102,9 +1146,9 @@ - (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldSetLineFragmentRect
11021146
CGFloat maxTotalHeight = MAX((maxAttachmentHeight + textBaselineToBottom), maxFont.lineHeight);
11031147
realBaselineOffset = (CGRectGetHeight(*lineFragmentUsedRect) - maxTotalHeight) / 2.f;
11041148
if (hasAttachment) {
1105-
[textStorage addAttribute:HippyVerticalAlignBaselineOffsetAttributeName
1106-
value:@(realBaselineOffset + textBaselineToBottom)
1107-
range:storageRange];
1149+
// Defer writing attribute to avoid mutating storage during layout
1150+
[_pendingAttachmentBaselineBottoms addObject:@{ HippyPendingRangeKey: [NSValue valueWithRange:storageRange],
1151+
HippyPendingValueKey: @(realBaselineOffset + textBaselineToBottom) }];
11081152
}
11091153
}
11101154

@@ -1146,9 +1190,9 @@ - (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldSetLineFragmentRect
11461190
break;
11471191
}
11481192
if (abs(offset) > .0f && !attrs[HippyShadowViewAttributeName]) {
1149-
// only set for Text
1150-
[textStorage addAttribute:NSBaselineOffsetAttributeName value:@(offset) range:range];
1151-
_needRelayoutText = YES;
1193+
// only set for Text; defer to avoid mutation during layout
1194+
[_pendingBaselineOffsets addObject:@{ HippyPendingRangeKey: [NSValue valueWithRange:range],
1195+
HippyPendingOffsetKey: @(offset) }];
11521196
}
11531197
}
11541198
}];

0 commit comments

Comments
 (0)