Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use NSTextStorageDelegate instead of method swizzling #520

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apple/MarkdownCommitHook.mm
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@
}

// apply markdown
auto newString = [utils parseMarkdown:nsAttributedString
withAttributes:defaultNSTextAttributes];
NSMutableAttributedString *newString = [nsAttributedString mutableCopy];
tomekzaw marked this conversation as resolved.
Show resolved Hide resolved
[utils applyFormatting:newString withDefaultTextAttributes:defaultNSTextAttributes];

// create a clone of the old TextInputState and update the
// attributed string box to point to the string with markdown
Expand Down Expand Up @@ -217,8 +217,8 @@
stateData.attributedStringBox);

// apply markdown
auto newString = [utils parseMarkdown:nsAttributedString
withAttributes:defaultNSTextAttributes];
NSMutableAttributedString *newString = [nsAttributedString mutableCopy];
[utils applyFormatting:newString withDefaultTextAttributes:defaultNSTextAttributes];

// create a clone of the old TextInputState and update the
// attributed string box to point to the string with markdown
Expand Down
15 changes: 15 additions & 0 deletions apple/MarkdownTextFieldObserver.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#import <UIKit/UIKit.h>
#import <React/RCTUITextField.h>
#import <RNLiveMarkdown/RCTMarkdownUtils.h>

NS_ASSUME_NONNULL_BEGIN

@interface MarkdownTextFieldObserver : NSObject

- (instancetype)initWithTextField:(nonnull RCTUITextField *)textField markdownUtils:(nonnull RCTMarkdownUtils *)markdownUtils;

- (void)textFieldDidChange:(UITextField *)textField;

@end

NS_ASSUME_NONNULL_END
47 changes: 47 additions & 0 deletions apple/MarkdownTextFieldObserver.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#import <RNLiveMarkdown/MarkdownTextFieldObserver.h>
#import "react_native_assert.h"

@implementation MarkdownTextFieldObserver {
RCTUITextField *_textField;
RCTMarkdownUtils *_markdownUtils;
BOOL _active;
}

- (instancetype)initWithTextField:(nonnull RCTUITextField *)textField markdownUtils:(nonnull RCTMarkdownUtils *)markdownUtils
{
if ((self = [super init])) {
react_native_assert(textField != nil);
react_native_assert(markdownUtils != nil);

_textField = textField;
_markdownUtils = markdownUtils;
_active = YES;
}
return self;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
if (_active && ([keyPath isEqualToString:@"text"] || [keyPath isEqualToString:@"attributedText"])) {
[self textFieldDidChange:_textField];
}
}

- (void)textFieldDidChange:(__unused UITextField *)textField {
react_native_assert(_textField.defaultTextAttributes != nil);

if (_textField.markedTextRange != nil) {
return; // skip formatting during multi-stage input to avoid breaking internal state
}

NSMutableAttributedString *attributedText = [textField.attributedText mutableCopy];
[_markdownUtils applyFormatting:attributedText withDefaultTextAttributes:_textField.defaultTextAttributes];

UITextRange *textRange = _textField.selectedTextRange;
_active = NO; // prevent recursion
_textField.attributedText = attributedText;
_active = YES;
[_textField setSelectedTextRange:textRange notifyDelegate:NO];
}

@end
108 changes: 65 additions & 43 deletions apple/MarkdownTextInputDecoratorView.mm
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
#import <React/RCTUITextField.h>
#import <React/RCTUITextView.h>
#import "react_native_assert.h"

#import <RNLiveMarkdown/MarkdownLayoutManager.h>
#import <RNLiveMarkdown/MarkdownTextInputDecoratorView.h>
#import <RNLiveMarkdown/RCTBackedTextFieldDelegateAdapter+Markdown.h>
#import <RNLiveMarkdown/RCTUITextView+Markdown.h>

#ifdef RCT_NEW_ARCH_ENABLED
#import <RNLiveMarkdown/RCTTextInputComponentView+Markdown.h>
#import <React/RCTTextInputComponentView.h>
#else
#import <RNLiveMarkdown/RCTBaseTextInputView+Markdown.h>
#endif /* RCT_NEW_ARCH_ENABLED */
#import <React/RCTBaseTextInputView.h>
#endif

#import <RNLiveMarkdown/MarkdownLayoutManager.h>
#import <RNLiveMarkdown/MarkdownTextInputDecoratorView.h>
#import <RNLiveMarkdown/MarkdownTextStorageDelegate.h>
#import <RNLiveMarkdown/MarkdownTextFieldObserver.h>

#import <objc/runtime.h>

@implementation MarkdownTextInputDecoratorView {
RCTMarkdownUtils *_markdownUtils;
RCTMarkdownStyle *_markdownStyle;
#ifdef RCT_NEW_ARCH_ENABLED
__weak RCTTextInputComponentView *_textInput;
#else
__weak RCTBaseTextInputView *_textInput;
#endif /* RCT_NEW_ARCH_ENABLED */
__weak UIView<RCTBackedTextInputViewProtocol> *_backedTextInputView;
__weak RCTBackedTextFieldDelegateAdapter *_adapter;
MarkdownTextStorageDelegate *_markdownTextStorageDelegate;
MarkdownTextFieldObserver *_markdownTextFieldObserver;
__weak RCTUITextView *_textView;
__weak RCTUITextField *_textField;
}

- (void)didMoveToWindow {
Expand All @@ -51,26 +48,51 @@ - (void)didMoveToWindow {

#ifdef RCT_NEW_ARCH_ENABLED
react_native_assert([view isKindOfClass:[RCTTextInputComponentView class]] && "Previous sibling component is not an instance of RCTTextInputComponentView.");
_textInput = (RCTTextInputComponentView *)view;
_backedTextInputView = [_textInput valueForKey:@"_backedTextInputView"];
RCTTextInputComponentView *textInputComponentView = (RCTTextInputComponentView *)view;
UIView<RCTBackedTextInputViewProtocol> *backedTextInputView = [textInputComponentView valueForKey:@"_backedTextInputView"];
#else
react_native_assert([view isKindOfClass:[RCTBaseTextInputView class]] && "Previous sibling component is not an instance of RCTBaseTextInputView.");
_textInput = (RCTBaseTextInputView *)view;
_backedTextInputView = _textInput.backedTextInputView;
RCTBaseTextInputView *baseTextInputView = (RCTBaseTextInputView *)view;
UIView<RCTBackedTextInputViewProtocol> *backedTextInputView = baseTextInputView.backedTextInputView;
#endif /* RCT_NEW_ARCH_ENABLED */

_markdownUtils = [[RCTMarkdownUtils alloc] init];
react_native_assert(_markdownStyle != nil);
[_markdownUtils setMarkdownStyle:_markdownStyle];

[_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];
if ([backedTextInputView isKindOfClass:[RCTUITextField class]]) {
_textField = (RCTUITextField *)backedTextInputView;

// make sure `adjustsFontSizeToFitWidth` is disabled, otherwise formatting will be overwritten
react_native_assert(_textField.adjustsFontSizeToFitWidth == NO);

_markdownTextFieldObserver = [[MarkdownTextFieldObserver alloc] initWithTextField:_textField markdownUtils:_markdownUtils];

// register observers for future edits
[_textField addTarget:_markdownTextFieldObserver action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged];
[_textField addObserver:_markdownTextFieldObserver forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:NULL];
[_textField addObserver:_markdownTextFieldObserver forKeyPath:@"attributedText" options:NSKeyValueObservingOptionNew context:NULL];
tomekzaw marked this conversation as resolved.
Show resolved Hide resolved

// format initial value
[_markdownTextFieldObserver textFieldDidChange:_textField];

// TODO: register blockquotes layout manager
tomekzaw marked this conversation as resolved.
Show resolved Hide resolved
// https://github.com/Expensify/react-native-live-markdown/issues/87
} else if ([backedTextInputView isKindOfClass:[RCTUITextView class]]) {
_textView = (RCTUITextView *)backedTextInputView;

// register delegate for future edits
tomekzaw marked this conversation as resolved.
Show resolved Hide resolved
react_native_assert(_textView.textStorage.delegate == nil);
_markdownTextStorageDelegate = [[MarkdownTextStorageDelegate alloc] initWithTextView:_textView markdownUtils:_markdownUtils];
_textView.textStorage.delegate = _markdownTextStorageDelegate;

#ifdef RCT_NEW_ARCH_ENABLED
// format initial value
[_textView.textStorage setAttributedString:_textView.attributedText];
#else
// `_textView.defaultTextAttributes` is nil here, initial value will be passed to `setAttributedText:` that will be called later
#endif

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)
Expand All @@ -90,18 +112,22 @@ - (void)didMoveToWindow {

- (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]);
}
_markdownTextStorageDelegate = nil;
_textView.textStorage.delegate = nil;
_textView = nil;
}

if (_textField != nil) {
[_textField removeTarget:_markdownTextFieldObserver action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged];
[_textField removeObserver:_markdownTextFieldObserver forKeyPath:@"text" context:NULL];
[_textField removeObserver:_markdownTextFieldObserver forKeyPath:@"attributedText" context:NULL];
_markdownTextFieldObserver = nil;
_textField = nil;
}
}

Expand All @@ -110,16 +136,12 @@ - (void)setMarkdownStyle:(RCTMarkdownStyle *)markdownStyle
_markdownStyle = markdownStyle;
[_markdownUtils setMarkdownStyle:markdownStyle];

// trigger reformatting
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
#ifdef RCT_NEW_ARCH_ENABLED
[_textInput _setAttributedString:_backedTextInputView.attributedText];
#else
[_textInput setAttributedText:_textInput.attributedText];
#endif /* RCT_NEW_ARCH_ENABLED */
[_textView.textStorage setAttributedString:_textView.attributedText];
}
if (_textField != nil) {
[_markdownTextFieldObserver textFieldDidChange:_textField];
}
}

Expand Down
13 changes: 13 additions & 0 deletions apple/MarkdownTextStorageDelegate.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#import <UIKit/UIKit.h>
#import <React/RCTUITextView.h>
#import <RNLiveMarkdown/RCTMarkdownUtils.h>

NS_ASSUME_NONNULL_BEGIN

@interface MarkdownTextStorageDelegate : NSObject <NSTextStorageDelegate>

- (instancetype)initWithTextView:(nonnull RCTUITextView *)textView markdownUtils:(nonnull RCTMarkdownUtils *)markdownUtils;

@end

NS_ASSUME_NONNULL_END
30 changes: 30 additions & 0 deletions apple/MarkdownTextStorageDelegate.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#import <RNLiveMarkdown/MarkdownTextStorageDelegate.h>
#import "react_native_assert.h"

@implementation MarkdownTextStorageDelegate {
RCTUITextView *_textView;
RCTMarkdownUtils *_markdownUtils;
}

- (instancetype)initWithTextView:(nonnull RCTUITextView *)textView markdownUtils:(nonnull RCTMarkdownUtils *)markdownUtils
{
if ((self = [super init])) {
react_native_assert(textView != nil);
react_native_assert(markdownUtils != nil);

_textView = textView;
_markdownUtils = markdownUtils;
}
return self;
}

- (void)textStorage:(NSTextStorage *)textStorage didProcessEditing:(NSTextStorageEditActions)editedMask range:(NSRange)editedRange changeInLength:(NSInteger)delta {
react_native_assert(_textView.defaultTextAttributes != nil);

[_markdownUtils applyFormatting:textStorage withDefaultTextAttributes:_textView.defaultTextAttributes];

// TODO: fix cursor position when adding newline after a blockquote (probably not here though)
// TODO: fix spellcheck not working for any of previous words when component value is controlled and contains bold (probably not here though)
}

@end
14 changes: 0 additions & 14 deletions apple/RCTBackedTextFieldDelegateAdapter+Markdown.h

This file was deleted.

43 changes: 0 additions & 43 deletions apple/RCTBackedTextFieldDelegateAdapter+Markdown.mm

This file was deleted.

18 changes: 0 additions & 18 deletions apple/RCTBaseTextInputView+Markdown.h

This file was deleted.

Loading
Loading