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 49 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
edce693
Use `NSTextStorageDelegate` instead of method swizzling
tomekzaw Oct 17, 2024
1ea65e4
Fix spellcheck
tomekzaw Oct 17, 2024
4214989
Merge branch 'main' into @tomekzaw/NSTextStorageDelegate
tomekzaw Oct 21, 2024
20166dd
Add singleline implementation
tomekzaw Oct 21, 2024
b089830
Improve multiline cleanup
tomekzaw Oct 21, 2024
d5cc035
Add TODO about spellcheck
tomekzaw Oct 21, 2024
80c0686
Add missing imports
tomekzaw Oct 21, 2024
0d57eeb
Add support for old architecture
tomekzaw Oct 21, 2024
5675e04
Add #ifdef for imports
tomekzaw Oct 21, 2024
e1fca80
Restore original App.tsx
tomekzaw Oct 23, 2024
5a1651f
Don't mix styles
tomekzaw Oct 23, 2024
1b016bf
Merge branch 'main' into @tomekzaw/NSTextStorageDelegate
tomekzaw Nov 5, 2024
e9e0e60
Update Podfile.lock
tomekzaw Nov 5, 2024
d0ef4ef
Assert that delegate is nil
tomekzaw Nov 5, 2024
8c06be0
Add comment with link to the issue
tomekzaw Nov 5, 2024
9ff01d5
Reformat singleline input on `markdownStyle` change
tomekzaw Nov 5, 2024
40abeba
Add nice constructors for `MarkdownTextStorageDelegate` and `Markdown…
tomekzaw Nov 5, 2024
67968ec
Fix emoji not visible inside text (seriously, this took like 3 hours)
tomekzaw Nov 5, 2024
d377043
Fix updating `style` without updating `markdownStyle` for multiline i…
tomekzaw Nov 5, 2024
6e40703
Add comments
tomekzaw Nov 5, 2024
9fc1d8e
Merge branch 'main' into @tomekzaw/NSTextStorageDelegate
tomekzaw Nov 6, 2024
1fa1b54
Update Podfile.lock
tomekzaw Nov 6, 2024
2518c5f
Merge branch 'main' into @tomekzaw/NSTextStorageDelegate
tomekzaw Nov 20, 2024
7b475de
Update Podfile.lock
tomekzaw Nov 20, 2024
1c432fe
Fix flickering and missing formatting when toggling multiline prop
tomekzaw Nov 20, 2024
2a37b85
Merge branch 'main' into @tomekzaw/NSTextStorageDelegate
tomekzaw Nov 21, 2024
7a13eac
Update Podfile.lock
tomekzaw Nov 21, 2024
3a6a58a
Update Podfile.lock
tomekzaw Nov 21, 2024
003ed5e
Update Podfile.lock
tomekzaw Nov 21, 2024
d402ca9
Merge branch 'main' into @tomekzaw/NSTextStorageDelegate
tomekzaw Nov 26, 2024
5cf9b9b
Update Podfile.lock
tomekzaw Nov 26, 2024
c00c440
Fix cursor position in new line after blockquote
tomekzaw Nov 26, 2024
7425d76
Remove trailing whitespace
tomekzaw Nov 27, 2024
511d885
Optimize updating paragraph style in typing attributes
tomekzaw Nov 27, 2024
ebd9813
Merge branch 'main' into @tomekzaw/NSTextStorageDelegate
tomekzaw Dec 9, 2024
b5c0e11
Remove outdated TODO
tomekzaw Dec 9, 2024
8400a91
Merge branch 'main' into @tomekzaw/NSTextStorageDelegate
tomekzaw Dec 10, 2024
4b05c62
Merge branch 'main' into @tomekzaw/NSTextStorageDelegate
tomekzaw Dec 10, 2024
0c381f6
Update Podfile.lock
tomekzaw Dec 10, 2024
1363312
Fix applying underline to the whole text on singleline input blur
tomekzaw Dec 10, 2024
1f53ff2
Eliminate underline blinks while typing if previous text ends with a …
tomekzaw Dec 10, 2024
7977d83
Introduce `applyMarkdownFormatting` method in `MarkdownTextFieldObser…
tomekzaw Dec 10, 2024
a3e9169
Fix underline blinks while typing after a link
tomekzaw Dec 11, 2024
1fb4a00
Merge branch 'main' into @tomekzaw/NSTextStorageDelegate
tomekzaw Dec 12, 2024
edfa822
Fix zombie blockquote ribbon and indent
tomekzaw Dec 12, 2024
03ec16f
Add comment
tomekzaw Dec 12, 2024
4c70fba
Fix regression with spellcheck disappearing immediately
tomekzaw Dec 12, 2024
b503121
Fix blockquote line height on iOS
tomekzaw Dec 16, 2024
63f2af2
Merge branch 'main' into @tomekzaw/NSTextStorageDelegate
tomekzaw Dec 21, 2024
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
19 changes: 19 additions & 0 deletions apple/MarkdownTextFieldObserver.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#import <UIKit/UIKit.h>
#import <React/RCTUITextField.h>
#import <RNLiveMarkdown/RCTMarkdownUtils.h>

NS_ASSUME_NONNULL_BEGIN

@interface MarkdownTextFieldObserver : NSObject

@property (nonatomic, nullable, strong) RCTMarkdownUtils *markdownUtils;

@property (nonatomic, nullable, strong) RCTUITextField *textField;
tomekzaw marked this conversation as resolved.
Show resolved Hide resolved

@property (nonatomic) BOOL active;
tomekzaw marked this conversation as resolved.
Show resolved Hide resolved

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

@end

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

@implementation MarkdownTextFieldObserver

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
react_native_assert(_textField != nil);

if (_active && ([keyPath isEqualToString:@"text"] || [keyPath isEqualToString:@"attributedText"])) {
[self textFieldDidChange:_textField];
}
}

- (void)textFieldDidChange:(__unused UITextField *)textField {
react_native_assert(_markdownUtils != nil);
react_native_assert(_textField != nil);
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
111 changes: 66 additions & 45 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,55 @@ - (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] init];
_markdownTextFieldObserver.markdownUtils = _markdownUtils;
_markdownTextFieldObserver.textField = _textField;
_markdownTextFieldObserver.active = YES;
tomekzaw marked this conversation as resolved.
Show resolved Hide resolved

// 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
} else if ([backedTextInputView isKindOfClass:[RCTUITextView class]]) {
_textView = (RCTUITextView *)backedTextInputView;

_markdownTextStorageDelegate = [[MarkdownTextStorageDelegate alloc] init];
_markdownTextStorageDelegate.markdownUtils = _markdownUtils;
_markdownTextStorageDelegate.textView = _textView;

// register delegate for future edits
tomekzaw marked this conversation as resolved.
Show resolved Hide resolved
_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 +116,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,17 +140,8 @@ - (void)setMarkdownStyle:(RCTMarkdownStyle *)markdownStyle
_markdownStyle = markdownStyle;
[_markdownUtils setMarkdownStyle:markdownStyle];

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 */
}
// trigger reformatting
[_textView.textStorage setAttributedString:_textView.attributedText];
tomekzaw marked this conversation as resolved.
Show resolved Hide resolved
}

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

NS_ASSUME_NONNULL_BEGIN

@interface MarkdownTextStorageDelegate : NSObject <NSTextStorageDelegate>

@property(nonatomic, nullable) RCTMarkdownUtils *markdownUtils;
tomekzaw marked this conversation as resolved.
Show resolved Hide resolved

@property(nonatomic, nullable, strong) RCTUITextView *textView;
tomekzaw marked this conversation as resolved.
Show resolved Hide resolved

@end

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

@implementation MarkdownTextStorageDelegate

- (void)textStorage:(NSTextStorage *)textStorage didProcessEditing:(NSTextStorageEditActions)editedMask range:(NSRange)editedRange changeInLength:(NSInteger)delta {
react_native_assert(_markdownUtils != nil);
react_native_assert(_textView != nil);
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