From 1da19969dd8cc8d146687da8bcaf1376bf3ac3c5 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Mon, 10 Mar 2025 12:35:49 +0100 Subject: [PATCH] Merge `MarkdownTextInputDecoratorView` into `MarkdownTextInputDecoratorComponentView` on iOS --- ...MarkdownTextInputDecoratorComponentView.mm | 126 ++++++++++++++++-- apple/MarkdownTextInputDecoratorView.h | 14 -- apple/MarkdownTextInputDecoratorView.mm | 110 --------------- .../MarkdownTextInputDecoratorViewManager.mm | 16 +-- 4 files changed, 114 insertions(+), 152 deletions(-) delete mode 100644 apple/MarkdownTextInputDecoratorView.h delete mode 100644 apple/MarkdownTextInputDecoratorView.mm diff --git a/apple/MarkdownTextInputDecoratorComponentView.mm b/apple/MarkdownTextInputDecoratorComponentView.mm index 354fae78a..70789c4ae 100644 --- a/apple/MarkdownTextInputDecoratorComponentView.mm +++ b/apple/MarkdownTextInputDecoratorComponentView.mm @@ -1,17 +1,29 @@ +#import #import +#import +#import +#import +#import #import -#import +#import +#import #import +#import +#import -#import -#import "MarkdownShadowFamilyRegistry.h" -#import "RCTFabricComponentsPlugins.h" +#import using namespace facebook::react; @implementation MarkdownTextInputDecoratorComponentView { - MarkdownTextInputDecoratorView *_view; + RCTMarkdownUtils *_markdownUtils; + RCTMarkdownStyle *_markdownStyle; + NSNumber *_parserId; + __weak RCTTextInputComponentView *_textInput; + __weak UIView *_backedTextInputView; + __weak RCTBackedTextFieldDelegateAdapter *_adapter; + __weak RCTUITextView *_textView; ShadowNodeFamily::Shared _decoratorFamily; } @@ -31,10 +43,6 @@ - (instancetype)initWithFrame:(CGRect)frame if (self = [super initWithFrame:frame]) { static const auto defaultProps = std::make_shared(); _props = defaultProps; - - _view = [[MarkdownTextInputDecoratorView alloc] init]; - - self.contentView = _view; } return self; @@ -43,11 +51,11 @@ - (instancetype)initWithFrame:(CGRect)frame - (void)updateState:(const facebook::react::State::Shared &)state oldState:(const facebook::react::State::Shared &)oldState { auto data = std::static_pointer_cast(state)->getData(); - + if (_decoratorFamily != nullptr) { MarkdownShadowFamilyRegistry::unregisterFamilyForUpdates(_decoratorFamily); } - + _decoratorFamily = data.decoratorFamily; MarkdownShadowFamilyRegistry::registerFamilyForUpdates(_decoratorFamily); } @@ -57,26 +65,116 @@ - (void)willMoveToSuperview:(UIView *)newSuperview { MarkdownShadowFamilyRegistry::unregisterFamilyForUpdates(_decoratorFamily); _decoratorFamily = nullptr; } - + [super willMoveToSuperview:newSuperview]; } +- (void)didMoveToWindow { + if (self.superview == nil) { + return; + } + + NSArray *viewsArray = self.superview.subviews; + NSUInteger currentIndex = [viewsArray indexOfObject:self]; + + react_native_assert(currentIndex != 0 && currentIndex != NSNotFound && "Error while finding current component."); + UIView *view = [viewsArray objectAtIndex:currentIndex - 1]; + + react_native_assert([view isKindOfClass:[RCTTextInputComponentView class]] && "Previous sibling component is not an instance of RCTTextInputComponentView."); + _textInput = (RCTTextInputComponentView *)view; + _backedTextInputView = [_textInput valueForKey:@"_backedTextInputView"]; + + _markdownUtils = [[RCTMarkdownUtils alloc] init]; + react_native_assert(_markdownStyle != nil); + [_markdownUtils setMarkdownStyle:_markdownStyle]; + [_markdownUtils setParserId:_parserId]; + + [_textInput setMarkdownUtils:_markdownUtils]; + if ([_backedTextInputView isKindOfClass:[RCTUITextField class]]) { + RCTUITextField *textField = (RCTUITextField *)_backedTextInputView; + _adapter = [textField valueForKey:@"textInputDelegateAdapter"]; + [_adapter setMarkdownUtils:_markdownUtils]; + } else if ([_backedTextInputView isKindOfClass:[RCTUITextView class]]) { + _textView = (RCTUITextView *)_backedTextInputView; + [_textView setMarkdownUtils:_markdownUtils]; + NSLayoutManager *layoutManager = _textView.layoutManager; // switching to TextKit 1 compatibility mode + + // Correct content height in TextKit 1 compatibility mode. (See https://github.com/Expensify/App/issues/41567) + // Consider removing this fix if it is no longer needed after migrating to TextKit 2. + CGSize contentSize = _textView.contentSize; + CGRect textBounds = [layoutManager usedRectForTextContainer:_textView.textContainer]; + contentSize.height = textBounds.size.height + _textView.textContainerInset.top + _textView.textContainerInset.bottom; + [_textView setContentSize:contentSize]; + + layoutManager.allowsNonContiguousLayout = NO; // workaround for onScroll issue + object_setClass(layoutManager, [MarkdownLayoutManager class]); + [layoutManager setValue:_markdownUtils forKey:@"markdownUtils"]; + } else { + react_native_assert(false && "Cannot enable Markdown for this type of TextInput."); + } +} + +- (void)willMoveToWindow:(UIWindow *)newWindow +{ + if (_textInput != nil) { + [_textInput setMarkdownUtils:nil]; + } + if (_adapter != nil) { + [_adapter setMarkdownUtils:nil]; + } + if (_textView != nil) { + [_textView setMarkdownUtils:nil]; + if (_textView.layoutManager != nil && [object_getClass(_textView.layoutManager) isEqual:[MarkdownLayoutManager class]]) { + [_textView.layoutManager setValue:nil forKey:@"markdownUtils"]; + object_setClass(_textView.layoutManager, [NSLayoutManager class]); + } + } +} + - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps { const auto &oldViewProps = *std::static_pointer_cast(_props); const auto &newViewProps = *std::static_pointer_cast(props); if (oldViewProps.parserId != newViewProps.parserId) { - [_view setParserId:@(newViewProps.parserId)]; + [self setParserId:@(newViewProps.parserId)]; } // TODO: if (oldViewProps.markdownStyle != newViewProps.markdownStyle) RCTMarkdownStyle *markdownStyle = [[RCTMarkdownStyle alloc] initWithStruct:newViewProps.markdownStyle]; - [_view setMarkdownStyle:markdownStyle]; + [self setMarkdownStyle:markdownStyle]; [super updateProps:props oldProps:oldProps]; } +- (void)setMarkdownStyle:(RCTMarkdownStyle *)markdownStyle +{ + // TODO: move to updateProps method + _markdownStyle = markdownStyle; + [_markdownUtils setMarkdownStyle:markdownStyle]; + [self applyNewStyles]; +} + +- (void)setParserId:(NSNumber *)parserId +{ + // TODO: move to updateProps method + _parserId = parserId; + [_markdownUtils setParserId:parserId]; + [self applyNewStyles]; +} + +- (void)applyNewStyles +{ + // TODO: call applyNewStyles only once if both markdownStyle and parserId change + if (_textView != nil) { + // We want to use `textStorage` for applying markdown when possible. Currently it's only available for UITextView + [_textView textDidChange]; + } else { + // apply new styles + [_textInput _setAttributedString:_backedTextInputView.attributedText]; + } +} + Class MarkdownTextInputDecoratorViewCls(void) { return MarkdownTextInputDecoratorComponentView.class; diff --git a/apple/MarkdownTextInputDecoratorView.h b/apple/MarkdownTextInputDecoratorView.h deleted file mode 100644 index 7d22469bc..000000000 --- a/apple/MarkdownTextInputDecoratorView.h +++ /dev/null @@ -1,14 +0,0 @@ -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface MarkdownTextInputDecoratorView : UIView - -- (void)setMarkdownStyle:(RCTMarkdownStyle *)markdownStyle; - -- (void)setParserId:(NSNumber *)parserId; - -@end - -NS_ASSUME_NONNULL_END diff --git a/apple/MarkdownTextInputDecoratorView.mm b/apple/MarkdownTextInputDecoratorView.mm deleted file mode 100644 index 49805cf91..000000000 --- a/apple/MarkdownTextInputDecoratorView.mm +++ /dev/null @@ -1,110 +0,0 @@ -#import -#import "react_native_assert.h" - -#import -#import -#import -#import - -#import - -#import - -@implementation MarkdownTextInputDecoratorView { - RCTMarkdownUtils *_markdownUtils; - RCTMarkdownStyle *_markdownStyle; - NSNumber *_parserId; - __weak RCTTextInputComponentView *_textInput; - __weak UIView *_backedTextInputView; - __weak RCTBackedTextFieldDelegateAdapter *_adapter; - __weak RCTUITextView *_textView; -} - -- (void)didMoveToWindow { - if (self.superview.superview == nil) { - return; - } - - NSArray *viewsArray = self.superview.superview.subviews; - NSUInteger currentIndex = [viewsArray indexOfObject:self.superview]; - - react_native_assert(currentIndex != 0 && currentIndex != NSNotFound && "Error while finding current component."); - UIView *view = [viewsArray objectAtIndex:currentIndex - 1]; - - react_native_assert([view isKindOfClass:[RCTTextInputComponentView class]] && "Previous sibling component is not an instance of RCTTextInputComponentView."); - _textInput = (RCTTextInputComponentView *)view; - _backedTextInputView = [_textInput valueForKey:@"_backedTextInputView"]; - - _markdownUtils = [[RCTMarkdownUtils alloc] init]; - react_native_assert(_markdownStyle != nil); - [_markdownUtils setMarkdownStyle:_markdownStyle]; - [_markdownUtils setParserId:_parserId]; - - [_textInput setMarkdownUtils:_markdownUtils]; - if ([_backedTextInputView isKindOfClass:[RCTUITextField class]]) { - RCTUITextField *textField = (RCTUITextField *)_backedTextInputView; - _adapter = [textField valueForKey:@"textInputDelegateAdapter"]; - [_adapter setMarkdownUtils:_markdownUtils]; - } else if ([_backedTextInputView isKindOfClass:[RCTUITextView class]]) { - _textView = (RCTUITextView *)_backedTextInputView; - [_textView setMarkdownUtils:_markdownUtils]; - NSLayoutManager *layoutManager = _textView.layoutManager; // switching to TextKit 1 compatibility mode - - // Correct content height in TextKit 1 compatibility mode. (See https://github.com/Expensify/App/issues/41567) - // Consider removing this fix if it is no longer needed after migrating to TextKit 2. - CGSize contentSize = _textView.contentSize; - CGRect textBounds = [layoutManager usedRectForTextContainer:_textView.textContainer]; - contentSize.height = textBounds.size.height + _textView.textContainerInset.top + _textView.textContainerInset.bottom; - [_textView setContentSize:contentSize]; - - layoutManager.allowsNonContiguousLayout = NO; // workaround for onScroll issue - object_setClass(layoutManager, [MarkdownLayoutManager class]); - [layoutManager setValue:_markdownUtils forKey:@"markdownUtils"]; - } else { - react_native_assert(false && "Cannot enable Markdown for this type of TextInput."); - } -} - -- (void)willMoveToWindow:(UIWindow *)newWindow -{ - if (_textInput != nil) { - [_textInput setMarkdownUtils:nil]; - } - if (_adapter != nil) { - [_adapter setMarkdownUtils:nil]; - } - if (_textView != nil) { - [_textView setMarkdownUtils:nil]; - if (_textView.layoutManager != nil && [object_getClass(_textView.layoutManager) isEqual:[MarkdownLayoutManager class]]) { - [_textView.layoutManager setValue:nil forKey:@"markdownUtils"]; - object_setClass(_textView.layoutManager, [NSLayoutManager class]); - } - } -} - -- (void)setMarkdownStyle:(RCTMarkdownStyle *)markdownStyle -{ - _markdownStyle = markdownStyle; - [_markdownUtils setMarkdownStyle:markdownStyle]; - [self applyNewStyles]; -} - -- (void)setParserId:(NSNumber *)parserId -{ - _parserId = parserId; - [_markdownUtils setParserId:parserId]; - [self applyNewStyles]; -} - -- (void)applyNewStyles -{ - if (_textView != nil) { - // We want to use `textStorage` for applying markdown when possible. Currently it's only available for UITextView - [_textView textDidChange]; - } else { - // apply new styles - [_textInput _setAttributedString:_backedTextInputView.attributedText]; - } -} - -@end diff --git a/apple/MarkdownTextInputDecoratorViewManager.mm b/apple/MarkdownTextInputDecoratorViewManager.mm index 319df771b..eaf320c21 100644 --- a/apple/MarkdownTextInputDecoratorViewManager.mm +++ b/apple/MarkdownTextInputDecoratorViewManager.mm @@ -1,23 +1,11 @@ #import -#import @implementation MarkdownTextInputDecoratorViewManager RCT_EXPORT_MODULE(MarkdownTextInputDecoratorView) -- (UIView *)view -{ - return [[MarkdownTextInputDecoratorView alloc] init]; -} +RCT_EXPORT_VIEW_PROPERTY(markdownStyle, NSDictionary) -RCT_CUSTOM_VIEW_PROPERTY(markdownStyle, NSDictionary, MarkdownTextInputDecoratorView) -{ - // implemented in MarkdownTextInputDecoratorView updateProps: -} - -RCT_CUSTOM_VIEW_PROPERTY(parserId, NSNumber, MarkdownTextInputDecoratorView) -{ - // implemented in MarkdownTextInputDecoratorView updateProps: -} +RCT_EXPORT_VIEW_PROPERTY(parserId, NSNumber) @end