Skip to content

Commit caba84e

Browse files
committed
[#4829] Add a patch that fixes complex input on iOS in RN 0.55.x.
The fix is described here: facebook/react-native#19809 The patch: 1. A copy of patched RCTUITextView.m file 2. A pre-action in the "Build" scheme to patch the RN file. See more on pre-actions here: https://burcugeneci.wordpress.com/2016/05/25/scripts-we-love-xcode-builds-life-savers/ 3. A build step to fail the build if the file wasn't patched. Signed-off-by: Igor Mandrigin <i@mandrigin.ru>
1 parent 499f5b0 commit caba84e

File tree

3 files changed

+314
-1
lines changed

3 files changed

+314
-1
lines changed

ios/StatusIm.xcodeproj/project.pbxproj

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,7 @@
11381138
isa = PBXNativeTarget;
11391139
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "StatusIm" */;
11401140
buildPhases = (
1141+
74D5DDAF20DA55D6002897BC /* ShellScript */,
11411142
2EAC54E16AB243C3EBBFE1BA /* [CP] Check Pods Manifest.lock */,
11421143
13B07F871A680F5B00A75B9A /* Sources */,
11431144
13B07F8C1A680F5B00A75B9A /* Frameworks */,
@@ -1830,6 +1831,19 @@
18301831
shellPath = /bin/sh;
18311832
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
18321833
};
1834+
74D5DDAF20DA55D6002897BC /* ShellScript */ = {
1835+
isa = PBXShellScriptBuildPhase;
1836+
buildActionMask = 2147483647;
1837+
files = (
1838+
);
1839+
inputPaths = (
1840+
);
1841+
outputPaths = (
1842+
);
1843+
runOnlyForDeploymentPostprocessing = 0;
1844+
shellPath = /bin/sh;
1845+
shellScript = "echo \"RN-PATCH: Checking if iOS React patch is applied...\"\n\nif cmp ${PROJECT_DIR}/../react-native-patches/RCTUITextView.m ${PROJECT_DIR}/../node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.m; then\n echo \"RN-PARCH: Patch was successfully applied\"\nelse\n echo \"RN-PATCH: Patch was NOT successfully applied\"\n exit 1\nfi";
1846+
};
18331847
967469AF8BA27D5CEC47B13C /* [CP] Copy Pods Resources */ = {
18341848
isa = PBXShellScriptBuildPhase;
18351849
buildActionMask = 2147483647;

ios/StatusIm.xcodeproj/xcshareddata/xcschemes/StatusIm.xcscheme

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,29 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
33
LastUpgradeVersion = "0830"
4-
version = "1.3">
4+
version = "1.7">
55
<BuildAction
66
parallelizeBuildables = "NO"
77
buildImplicitDependencies = "YES">
8+
<PreActions>
9+
<ExecutionAction
10+
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
11+
<ActionContent
12+
title = "Run Script"
13+
scriptText = "exec &gt; ${PROJECT_DIR}/prebuild.log 2&gt;&amp;1&#10;&#10;set -e&#10;&#10;echo &quot;Pre-action: patching react files!&quot;&#10;&#10;cp \&#10;${PROJECT_DIR}/../react-native-patches/RCTUITextView.m \&#10;${PROJECT_DIR}/../node_modules/react-native/Libraries/Text/TextInput/Multiline/&#10;&#10;echo &quot;Pre-action: patching done successully!&quot;"
14+
shellToInvoke = "/bin/sh">
15+
<EnvironmentBuildable>
16+
<BuildableReference
17+
BuildableIdentifier = "primary"
18+
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
19+
BuildableName = "StatusIm.app"
20+
BlueprintName = "StatusIm"
21+
ReferencedContainer = "container:StatusIm.xcodeproj">
22+
</BuildableReference>
23+
</EnvironmentBuildable>
24+
</ActionContent>
25+
</ExecutionAction>
26+
</PreActions>
827
<BuildActionEntries>
928
<BuildActionEntry
1029
buildForTesting = "YES"

react-native-patches/RCTUITextView.m

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import "RCTUITextView.h"
9+
10+
#import <React/RCTUtils.h>
11+
#import <React/UIView+React.h>
12+
13+
#import "RCTBackedTextInputDelegateAdapter.h"
14+
15+
@implementation RCTUITextView
16+
{
17+
UILabel *_placeholderView;
18+
UITextView *_detachedTextView;
19+
RCTBackedTextViewDelegateAdapter *_textInputDelegateAdapter;
20+
}
21+
22+
static UIFont *defaultPlaceholderFont()
23+
{
24+
return [UIFont systemFontOfSize:17];
25+
}
26+
27+
static UIColor *defaultPlaceholderColor()
28+
{
29+
// Default placeholder color from UITextField.
30+
return [UIColor colorWithRed:0 green:0 blue:0.0980392 alpha:0.22];
31+
}
32+
33+
- (instancetype)initWithFrame:(CGRect)frame
34+
{
35+
if (self = [super initWithFrame:frame]) {
36+
[[NSNotificationCenter defaultCenter] addObserver:self
37+
selector:@selector(textDidChange)
38+
name:UITextViewTextDidChangeNotification
39+
object:self];
40+
41+
_placeholderView = [[UILabel alloc] initWithFrame:self.bounds];
42+
_placeholderView.isAccessibilityElement = NO;
43+
_placeholderView.numberOfLines = 0;
44+
_placeholderView.textColor = defaultPlaceholderColor();
45+
[self addSubview:_placeholderView];
46+
47+
_textInputDelegateAdapter = [[RCTBackedTextViewDelegateAdapter alloc] initWithTextView:self];
48+
}
49+
50+
return self;
51+
}
52+
53+
- (void)dealloc
54+
{
55+
[[NSNotificationCenter defaultCenter] removeObserver:self];
56+
}
57+
58+
- (NSString *)accessibilityLabel
59+
{
60+
NSMutableString *accessibilityLabel = [NSMutableString new];
61+
62+
NSString *superAccessibilityLabel = [super accessibilityLabel];
63+
if (superAccessibilityLabel.length > 0) {
64+
[accessibilityLabel appendString:superAccessibilityLabel];
65+
}
66+
67+
if (self.placeholder.length > 0 && self.attributedText.string.length == 0) {
68+
if (accessibilityLabel.length > 0) {
69+
[accessibilityLabel appendString:@" "];
70+
}
71+
[accessibilityLabel appendString:self.placeholder];
72+
}
73+
74+
return accessibilityLabel;
75+
}
76+
77+
#pragma mark - Properties
78+
79+
- (void)setPlaceholder:(NSString *)placeholder
80+
{
81+
_placeholder = placeholder;
82+
_placeholderView.text = _placeholder;
83+
}
84+
85+
- (void)setPlaceholderColor:(UIColor *)placeholderColor
86+
{
87+
_placeholderColor = placeholderColor;
88+
_placeholderView.textColor = _placeholderColor ?: defaultPlaceholderColor();
89+
}
90+
91+
- (void)textDidChange
92+
{
93+
_textWasPasted = NO;
94+
[self invalidatePlaceholderVisibility];
95+
}
96+
97+
#pragma mark - Overrides
98+
99+
- (void)setFont:(UIFont *)font
100+
{
101+
[super setFont:font];
102+
_placeholderView.font = font ?: defaultPlaceholderFont();
103+
}
104+
105+
- (void)setTextAlignment:(NSTextAlignment)textAlignment
106+
{
107+
[super setTextAlignment:textAlignment];
108+
_placeholderView.textAlignment = textAlignment;
109+
}
110+
111+
- (void)setText:(NSString *)text
112+
{
113+
[super setText:text];
114+
[self textDidChange];
115+
}
116+
117+
- (void)setAttributedText:(NSAttributedString *)attributedText
118+
{
119+
// Using `setAttributedString:` while user is typing breaks some internal mechanics
120+
// when entering complex input languages such as Chinese, Korean or Japanese.
121+
// see: https://github.com/facebook/react-native/issues/19339
122+
123+
// We try to avoid calling this method as much as we can.
124+
// If the text has changed, there is nothing we can do.
125+
if (![super.attributedText.string isEqualToString:attributedText.string]) {
126+
[super setAttributedText:attributedText];
127+
} else {
128+
// But if the text is preserved, we just copying the attributes from the source string.
129+
if (![super.attributedText isEqualToAttributedString:attributedText]) {
130+
[self copyTextAttributesFrom:attributedText];
131+
}
132+
}
133+
134+
[self textDidChange];
135+
}
136+
137+
#pragma mark - Overrides
138+
139+
- (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BOOL)notifyDelegate
140+
{
141+
if (!notifyDelegate) {
142+
// We have to notify an adapter that following selection change was initiated programmatically,
143+
// so the adapter must not generate a notification for it.
144+
[_textInputDelegateAdapter skipNextTextInputDidChangeSelectionEventWithTextRange:selectedTextRange];
145+
}
146+
147+
[super setSelectedTextRange:selectedTextRange];
148+
}
149+
150+
- (void)paste:(id)sender
151+
{
152+
[super paste:sender];
153+
_textWasPasted = YES;
154+
}
155+
156+
- (void)setContentOffset:(CGPoint)contentOffset animated:(__unused BOOL)animated
157+
{
158+
// Turning off scroll animation.
159+
// This fixes the problem also known as "flaky scrolling".
160+
[super setContentOffset:contentOffset animated:NO];
161+
}
162+
163+
#pragma mark - Layout
164+
165+
- (CGFloat)preferredMaxLayoutWidth
166+
{
167+
// Returning size DOES contain `textContainerInset` (aka `padding`).
168+
return _preferredMaxLayoutWidth ?: self.placeholderSize.width;
169+
}
170+
171+
- (CGSize)placeholderSize
172+
{
173+
UIEdgeInsets textContainerInset = self.textContainerInset;
174+
NSString *placeholder = self.placeholder ?: @"";
175+
CGSize placeholderSize = [placeholder sizeWithAttributes:@{NSFontAttributeName: self.font ?: defaultPlaceholderFont()}];
176+
placeholderSize = CGSizeMake(RCTCeilPixelValue(placeholderSize.width), RCTCeilPixelValue(placeholderSize.height));
177+
placeholderSize.width += textContainerInset.left + textContainerInset.right;
178+
placeholderSize.height += textContainerInset.top + textContainerInset.bottom;
179+
// Returning size DOES contain `textContainerInset` (aka `padding`; as `sizeThatFits:` does).
180+
return placeholderSize;
181+
}
182+
183+
- (CGSize)contentSize
184+
{
185+
CGSize contentSize = super.contentSize;
186+
CGSize placeholderSize = self.placeholderSize;
187+
// When a text input is empty, it actually displays a placehoder.
188+
// So, we have to consider `placeholderSize` as a minimum `contentSize`.
189+
// Returning size DOES contain `textContainerInset` (aka `padding`).
190+
return CGSizeMake(
191+
MAX(contentSize.width, placeholderSize.width),
192+
MAX(contentSize.height, placeholderSize.height));
193+
}
194+
195+
- (void)layoutSubviews
196+
{
197+
[super layoutSubviews];
198+
199+
CGRect textFrame = UIEdgeInsetsInsetRect(self.bounds, self.textContainerInset);
200+
CGFloat placeholderHeight = [_placeholderView sizeThatFits:textFrame.size].height;
201+
textFrame.size.height = MIN(placeholderHeight, textFrame.size.height);
202+
_placeholderView.frame = textFrame;
203+
}
204+
205+
- (CGSize)intrinsicContentSize
206+
{
207+
// Returning size DOES contain `textContainerInset` (aka `padding`).
208+
return [self sizeThatFits:CGSizeMake(self.preferredMaxLayoutWidth, CGFLOAT_MAX)];
209+
}
210+
211+
- (CGSize)sizeThatFits:(CGSize)size
212+
{
213+
// Returned fitting size depends on text size and placeholder size.
214+
CGSize textSize = [self fixedSizeThatFits:size];
215+
CGSize placeholderSize = self.placeholderSize;
216+
// Returning size DOES contain `textContainerInset` (aka `padding`).
217+
return CGSizeMake(MAX(textSize.width, placeholderSize.width), MAX(textSize.height, placeholderSize.height));
218+
}
219+
220+
- (CGSize)fixedSizeThatFits:(CGSize)size
221+
{
222+
// UITextView on iOS 8 has a bug that automatically scrolls to the top
223+
// when calling `sizeThatFits:`. Use a copy so that self is not screwed up.
224+
static BOOL useCustomImplementation = NO;
225+
static dispatch_once_t onceToken;
226+
dispatch_once(&onceToken, ^{
227+
useCustomImplementation = ![[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){9,0,0}];
228+
});
229+
230+
if (!useCustomImplementation) {
231+
return [super sizeThatFits:size];
232+
}
233+
234+
if (!_detachedTextView) {
235+
_detachedTextView = [UITextView new];
236+
}
237+
238+
_detachedTextView.attributedText = self.attributedText;
239+
_detachedTextView.font = self.font;
240+
_detachedTextView.textContainerInset = self.textContainerInset;
241+
242+
return [_detachedTextView sizeThatFits:size];
243+
}
244+
245+
#pragma mark - Context Menu
246+
247+
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
248+
{
249+
if (_contextMenuHidden) {
250+
return NO;
251+
}
252+
253+
return [super canPerformAction:action withSender:sender];
254+
}
255+
256+
#pragma mark - Placeholder
257+
258+
- (void)invalidatePlaceholderVisibility
259+
{
260+
BOOL isVisible = _placeholder.length != 0 && self.attributedText.length == 0;
261+
_placeholderView.hidden = !isVisible;
262+
}
263+
264+
#pragma mark - Utility Methods
265+
266+
- (void)copyTextAttributesFrom:(NSAttributedString *)sourceString
267+
{
268+
[self.textStorage beginEditing];
269+
270+
NSTextStorage *textStorage = self.textStorage;
271+
[sourceString enumerateAttributesInRange:NSMakeRange(0, sourceString.length)
272+
options:NSAttributedStringEnumerationReverse
273+
usingBlock:^(NSDictionary<NSAttributedStringKey,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
274+
[textStorage setAttributes:attrs range:range];
275+
}];
276+
277+
[self.textStorage endEditing];
278+
}
279+
280+
@end

0 commit comments

Comments
 (0)