Skip to content

Commit

Permalink
iOS works (using method swizzling, for all TextInputs)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomekzaw committed Dec 5, 2023
1 parent a8f65ca commit 167720e
Show file tree
Hide file tree
Showing 12 changed files with 296 additions and 3 deletions.
2 changes: 1 addition & 1 deletion example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,7 @@ SPEC CHECKSUMS:
React-jsiexecutor: c49502e5d02112247ee4526bc3ccfc891ae3eb9b
React-jsinspector: 8baadae51f01d867c3921213a25ab78ab4fbcd91
React-logger: 8edc785c47c8686c7962199a307015e2ce9a0e4f
react-native-markdown-text-input: a3cb99c7c696c22d1359ffb1adb70312594baecf
react-native-markdown-text-input: 621ea125f6509bba437f00a1ab9cedd0dd0f1520
React-NativeModulesApple: b6868ee904013a7923128892ee4a032498a1024a
React-perflogger: 31ea61077185eb1428baf60c0db6e2886f141a5a
React-RCTActionSheet: 392090a3abc8992eb269ef0eaa561750588fc39d
Expand Down
22 changes: 21 additions & 1 deletion ios/MarkdownTextInputViewManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@
#import "RCTBridge.h"
#import "Utils.h"

#import <react-native-markdown-text-input/RCTMarkdownUtils.h>

@interface MarkdownTextInputView : UIView
@end

@implementation MarkdownTextInputView

- (void)didMoveToWindow {
NSArray *viewsArray = self.superview.subviews;
NSUInteger currentIndex = [viewsArray indexOfObject:self];
if (currentIndex == 0 || currentIndex == NSNotFound) {
return;
}
UIView *found = [viewsArray objectAtIndex:currentIndex - 1];
// TODO: enable Markdown only for this specific view
return;
}

@end

@interface MarkdownTextInputViewManager : RCTViewManager
@end

Expand All @@ -12,7 +32,7 @@ @implementation MarkdownTextInputViewManager

- (UIView *)view
{
return [[UIView alloc] init];
return [[MarkdownTextInputView alloc] init];
}

RCT_CUSTOM_VIEW_PROPERTY(color, NSString, UIView)
Expand Down
11 changes: 11 additions & 0 deletions ios/RCTBackedTextFieldDelegateAdapter+Markdown.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#import <React/RCTBackedTextInputDelegateAdapter.h>

NS_ASSUME_NONNULL_BEGIN

@interface RCTBackedTextFieldDelegateAdapter (Markdown)

- (void)markdown_textFieldDidChange;

@end

NS_ASSUME_NONNULL_END
32 changes: 32 additions & 0 deletions ios/RCTBackedTextFieldDelegateAdapter+Markdown.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#import <react-native-markdown-text-input/RCTBackedTextFieldDelegateAdapter+Markdown.h>
#import <react-native-markdown-text-input/RCTMarkdownUtils.h>
#import <React/RCTUITextField.h>
#import <objc/message.h>

@implementation RCTBackedTextFieldDelegateAdapter (Markdown)

- (void)markdown_textFieldDidChange
{
RCTUITextField *backedTextInputView = [self valueForKey:@"_backedTextInputView"];
UITextRange *range = backedTextInputView.selectedTextRange;
backedTextInputView.attributedText = [RCTMarkdownUtils parseMarkdown:backedTextInputView.attributedText.string];
[backedTextInputView setSelectedTextRange:range notifyDelegate:YES];

// Call the original method
[self markdown_textFieldDidChange];
}

+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [self class];
SEL originalSelector = @selector(textFieldDidChange);
SEL swizzledSelector = @selector(markdown_textFieldDidChange);
Method originalMethod = class_getInstanceMethod(cls, originalSelector);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}

@end
11 changes: 11 additions & 0 deletions ios/RCTBaseTextInputView+Markdown.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#import <React/RCTBaseTextInputView.h>

NS_ASSUME_NONNULL_BEGIN

@interface RCTBaseTextInputView (Markdown)

- (void)markdown_setAttributedText:(NSAttributedString *)attributedText;

@end

NS_ASSUME_NONNULL_END
30 changes: 30 additions & 0 deletions ios/RCTBaseTextInputView+Markdown.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#import <react-native-markdown-text-input/RCTBaseTextInputView+Markdown.h>
#import <react-native-markdown-text-input/RCTMarkdownUtils.h>
#import <objc/message.h>

@implementation RCTBaseTextInputView (Markdown)

- (void)markdown_setAttributedText:(NSAttributedString *)attributedText
{
if (attributedText != nil) {
attributedText = [RCTMarkdownUtils parseMarkdown:attributedText.string];
}

// Call the original method
[self markdown_setAttributedText:attributedText];
}

+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [self class];
SEL originalSelector = @selector(setAttributedText:);
SEL swizzledSelector = @selector(markdown_setAttributedText:);
Method originalMethod = class_getInstanceMethod(cls, originalSelector);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}

@end
5 changes: 5 additions & 0 deletions ios/RCTMarkdownUtils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@interface RCTMarkdownUtils : NSObject

+ (nonnull NSAttributedString *)parseMarkdown:(nonnull NSString *)input;

@end
91 changes: 91 additions & 0 deletions ios/RCTMarkdownUtils.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#import <react-native-markdown-text-input/RCTMarkdownUtils.h>
#import <JavaScriptCore/JavaScriptCore.h>

@implementation RCTMarkdownUtils

+ (NSAttributedString *)parseMarkdown:(nonnull NSString *)input {
static JSContext *ctx = nil;
static JSValue *function = nil;
if (ctx == nil) {
NSString *path = [[NSBundle mainBundle] pathForResource:@"out" ofType:@"js"];
NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL];
ctx = [[JSContext alloc] init];
[ctx evaluateScript:content];
function = ctx[@"parseMarkdownToTextAndRanges"];
}

JSValue *result = [function callWithArguments:@[input]];
NSString *text = [result[0] toString];
NSArray *ranges = [result[1] toArray];

NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:text];
[attributedString beginEditing];

[attributedString addAttribute:NSFontAttributeName
value:[UIFont systemFontOfSize:20]
range:NSMakeRange(0, [text length])];

NSMutableArray *quoteRanges = [NSMutableArray new];

[ranges enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSArray *item = obj;
NSString *type = item[0];
NSUInteger location = [item[1] unsignedIntegerValue];
NSUInteger length = [item[2] unsignedIntegerValue];
NSRange range = NSMakeRange(location, length);

UIFont *font = [attributedString attribute:NSFontAttributeName atIndex:location effectiveRange:NULL];
UIFontDescriptor *fontDescriptor = [font fontDescriptor];
UIFontDescriptorSymbolicTraits existingTraits = fontDescriptor.symbolicTraits;
UIFontDescriptorSymbolicTraits desiredTraits = UIFontDescriptorClassMask;

if ([type isEqualToString:@"bold"] || [type isEqualToString:@"mention"] || [type isEqualToString:@"h1"]) {
desiredTraits = existingTraits | UIFontDescriptorTraitBold;
} else if ([type isEqualToString:@"italic"]) {
desiredTraits = existingTraits | UIFontDescriptorTraitItalic;
} else if ([type isEqualToString:@"code"] || [type isEqualToString:@"pre"]) {
desiredTraits = existingTraits | UIFontDescriptorTraitMonoSpace;
} else if ([type isEqualToString:@"syntax"]) {
desiredTraits = UIFontDescriptorTraitBold; // TODO: remove italic in nested bold+italic
}

UIFontDescriptor *newFontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:desiredTraits];
CGFloat size = 0; // Passing 0 to size keeps the existing size
if ([type isEqualToString:@"h1"]) {
size = 25;
}
UIFont *newFont = [UIFont fontWithDescriptor:newFontDescriptor size:size];
[attributedString addAttribute:NSFontAttributeName value:newFont range:range];

if ([type isEqualToString:@"syntax"]) {
[attributedString addAttribute:NSForegroundColorAttributeName value:[UIColor grayColor] range:range];
} else if ([type isEqualToString:@"strikethrough"]) {
[attributedString addAttribute:NSStrikethroughStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range];
} else if ([type isEqualToString:@"code"]) {
[attributedString addAttribute:NSForegroundColorAttributeName value:[[UIColor alloc] initWithRed:6/255.0 green:25/255.0 blue:109/255.0 alpha:1.0] range:range];
[attributedString addAttribute:NSBackgroundColorAttributeName value:[[UIColor alloc] initWithRed:0.95 green:0.95 blue:0.95 alpha:1.0] range:range];
} else if ([type isEqualToString:@"mention"]) {
[attributedString addAttribute:NSBackgroundColorAttributeName value:[[UIColor alloc] initWithRed:252/255.0 green:232/255.0 blue:142/255.0 alpha:1.0] range:range];
} else if ([type isEqualToString:@"link"]) {
[attributedString addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range];
[attributedString addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:range];
} else if ([type isEqualToString:@"blockquote"]) {
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.firstLineHeadIndent = 11;
paragraphStyle.headIndent = 11;
[attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range];
[quoteRanges addObject:[NSValue valueWithRange:range]];
} else if ([type isEqualToString:@"pre"]) {
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.firstLineHeadIndent = 5;
paragraphStyle.headIndent = 5;
[attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range];
}
}];

[attributedString endEditing];

return attributedString;
}

@end
13 changes: 13 additions & 0 deletions ios/RCTUITextView+Markdown.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#import <UIKit/UIKit.h>

#import <React/RCTUITextView.h>

NS_ASSUME_NONNULL_BEGIN

@interface RCTUITextView (Markdown)

- (void)markdown_textDidChange;

@end

NS_ASSUME_NONNULL_END
31 changes: 31 additions & 0 deletions ios/RCTUITextView+Markdown.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#import <react-native-markdown-text-input/RCTUITextView+Markdown.h>
#import <react-native-markdown-text-input/RCTMarkdownUtils.h>
#import <objc/message.h>

@implementation RCTUITextView (Markdown)

- (void)markdown_textDidChange
{
UITextRange *range = self.selectedTextRange;
super.attributedText = [RCTMarkdownUtils parseMarkdown:self.attributedText.string];
[super setSelectedTextRange:range]; // prevents cursor from jumping at the end when typing in the middle of the text
self.typingAttributes = @{NSFontAttributeName: [UIFont boldSystemFontOfSize:20]}; // removes indent in new line when typing after quote

// Call the original method
[self markdown_textDidChange];
}

+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [self class];
SEL originalSelector = @selector(textDidChange);
SEL swizzledSelector = @selector(markdown_textDidChange);
Method originalMethod = class_getInstanceMethod(cls, originalSelector);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}

@end
47 changes: 47 additions & 0 deletions ios/out.js

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion react-native-markdown-text-input.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Pod::Spec.new do |s|

s.source_files = "ios/**/*.{h,m,mm}"

s.resources = "ios/out.js"

# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
# See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
if respond_to?(:install_modules_dependencies, true)
Expand All @@ -38,5 +40,5 @@ Pod::Spec.new do |s|
s.dependency "RCTTypeSafety"
s.dependency "ReactCommon/turbomodule/core"
end
end
end
end

0 comments on commit 167720e

Please sign in to comment.