Skip to content

Commit b661749

Browse files
authored
fix(iOS): fix issues with keyboard on autoFocus=true (#170)
1 parent b1694d5 commit b661749

31 files changed

Lines changed: 454 additions & 84 deletions

packages/react-native/Libraries/AppDelegate/RCTDefaultReactNativeFactoryDelegate.mm

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#import "RCTDefaultReactNativeFactoryDelegate.h"
99
#import <ReactCommon/RCTHost.h>
10+
#import <React/RCTViewController.h>
1011
#import "RCTAppSetupUtils.h"
1112
#import "RCTDependencyProvider.h"
1213
#if USE_THIRD_PARTY_JSC != 1
@@ -28,7 +29,7 @@ - (NSURL *_Nullable)sourceURLForBridge:(nonnull RCTBridge *)bridge
2829

2930
- (UIViewController *)createRootViewController
3031
{
31-
return [UIViewController new];
32+
return [RCTViewController new];
3233
}
3334

3435
- (RCTBridge *)createBridgeWithDelegate:(id<RCTBridgeDelegate>)delegate launchOptions:(NSDictionary *)launchOptions

packages/react-native/Libraries/AppDelegate/RCTReactNativeFactory.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@
1515
#import "RCTUIConfiguratorProtocol.h"
1616

1717
#if defined(__cplusplus) // Don't conform to protocols requiring C++ when it's not defined.
18+
#include <memory>
1819
#import <React/RCTComponentViewFactory.h>
1920
#import <ReactCommon/RCTHost.h>
2021
#import <ReactCommon/RCTTurboModuleManager.h>
2122

23+
namespace facebook::react {
24+
class ReactNativeFeatureFlagsProvider;
25+
}
26+
2227
#endif
2328

2429
@class RCTBridge;
@@ -90,6 +95,12 @@ typedef NS_ENUM(NSInteger, RCTReleaseLevel) { Canary, Experimental, Stable };
9095

9196
- (instancetype)initWithDelegate:(id<RCTReactNativeFactoryDelegate>)delegate releaseLevel:(RCTReleaseLevel)releaseLevel;
9297

98+
#if defined(__cplusplus)
99+
/// Initializes React Native with the provided feature flag provider.
100+
- (instancetype)initWithDelegate:(id<RCTReactNativeFactoryDelegate>)delegate
101+
featureFlagsProvider:(std::unique_ptr<facebook::react::ReactNativeFeatureFlagsProvider>)featureFlagsProvider;
102+
#endif
103+
93104
- (void)startReactNativeWithModuleName:(NSString *)moduleName inWindow:(UIWindow *_Nullable)window;
94105

95106
- (void)startReactNativeWithModuleName:(NSString *)moduleName

packages/react-native/Libraries/AppDelegate/RCTReactNativeFactory.mm

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#import <React/RCTUtils.h>
1414
#import <ReactCommon/RCTHost.h>
1515
#import <objc/runtime.h>
16+
#import <react/featureflags/ReactNativeFeatureFlags.h>
1617
#import <react/featureflags/ReactNativeFeatureFlagsOverridesOSSCanary.h>
1718
#import <react/featureflags/ReactNativeFeatureFlagsOverridesOSSExperimental.h>
1819
#import <react/featureflags/ReactNativeFeatureFlagsOverridesOSSStable.h>
@@ -51,23 +52,39 @@ - (instancetype)initWithDelegate:(id<RCTReactNativeFactoryDelegate>)delegate rel
5152
if (self = [super init]) {
5253
self.delegate = delegate;
5354
[self _setUpFeatureFlags:releaseLevel];
55+
[self _setUpReactNativeFactory];
56+
}
5457

55-
auto newArchEnabled = [self newArchEnabled];
56-
auto fabricEnabled = [self fabricEnabled];
57-
58-
[RCTColorSpaceUtils applyDefaultColorSpace:[self defaultColorSpace]];
59-
RCTEnableTurboModule([self turboModuleEnabled]);
60-
61-
self.rootViewFactory = [self createRCTRootViewFactory];
58+
return self;
59+
}
6260

63-
if (newArchEnabled || fabricEnabled) {
64-
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
65-
}
61+
- (instancetype)initWithDelegate:(id<RCTReactNativeFactoryDelegate>)delegate
62+
featureFlagsProvider:(std::unique_ptr<ReactNativeFeatureFlagsProvider>)featureFlagsProvider
63+
{
64+
if (self = [super init]) {
65+
self.delegate = delegate;
66+
[self _setUpFeatureFlagsWithProvider:std::move(featureFlagsProvider)];
67+
[self _setUpReactNativeFactory];
6668
}
6769

6870
return self;
6971
}
7072

73+
- (void)_setUpReactNativeFactory
74+
{
75+
auto newArchEnabled = [self newArchEnabled];
76+
auto fabricEnabled = [self fabricEnabled];
77+
78+
[RCTColorSpaceUtils applyDefaultColorSpace:[self defaultColorSpace]];
79+
RCTEnableTurboModule([self turboModuleEnabled]);
80+
81+
self.rootViewFactory = [self createRCTRootViewFactory];
82+
83+
if (newArchEnabled || fabricEnabled) {
84+
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
85+
}
86+
}
87+
7188
- (void)startReactNativeWithModuleName:(NSString *)moduleName inWindow:(UIWindow *_Nullable)window
7289
{
7390
[self startReactNativeWithModuleName:moduleName inWindow:window initialProperties:nil launchOptions:nil];
@@ -348,4 +365,21 @@ - (void)_setUpFeatureFlags:(RCTReleaseLevel)releaseLevel
348365
});
349366
}
350367

368+
- (void)_setUpFeatureFlagsWithProvider:(std::unique_ptr<ReactNativeFeatureFlagsProvider>)featureFlagsProvider
369+
{
370+
if (featureFlagsProvider == nullptr) {
371+
[NSException
372+
raise:@"RCTReactNativeFactory::_setUpFeatureFlagsWithProvider called with a null provider"
373+
format:@"A ReactNativeFeatureFlagsProvider must be provided"];
374+
return;
375+
}
376+
377+
__block auto featureFlagsProviderBlock = std::move(featureFlagsProvider);
378+
379+
static dispatch_once_t setupFeatureFlagsToken;
380+
dispatch_once(&setupFeatureFlagsToken, ^{
381+
ReactNativeFeatureFlags::override(std::move(featureFlagsProviderBlock));
382+
});
383+
}
384+
351385
@end

packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
#import <React/RCTUITextField.h>
1818
#import <React/RCTUITextView.h>
1919
#import <React/RCTUtils.h>
20+
#import <React/UIView+React.h>
21+
#import <React/UIViewController+React.h>
2022

2123
#import "RCTConversions.h"
2224
#import "RCTTextInputNativeCommands.h"
@@ -32,6 +34,7 @@
3234

3335
@interface RCTTextInputComponentView () <
3436
RCTBackedTextInputDelegate,
37+
RCTViewControllerAppearanceListener,
3538
RCTTextInputViewProtocol,
3639
UIDropInteractionDelegate>
3740
@end
@@ -66,6 +69,8 @@ @implementation RCTTextInputComponentView {
6669
*/
6770
BOOL _comingFromJS;
6871
BOOL _didMoveToWindow;
72+
BOOL _didAutoFocus;
73+
__weak UIViewController *_viewController;
6974

7075
/*
7176
* Newly initialized default typing attributes contain a no-op NSParagraphStyle and NSShadow. These cause inequality
@@ -91,6 +96,7 @@ - (instancetype)initWithFrame:(CGRect)frame
9196
_ignoreNextTextInputCall = NO;
9297
_comingFromJS = NO;
9398
_didMoveToWindow = NO;
99+
_didAutoFocus = NO;
94100
_originalTypingAttributes = [_backedTextInputView.typingAttributes copy];
95101
_previousContentSize = CGSizeZero;
96102

@@ -117,13 +123,18 @@ - (void)didMoveToWindow
117123
{
118124
[super didMoveToWindow];
119125

126+
bool enableNewAutoFocusImpl = ReactNativeFeatureFlags::enableIOSExperimentalAutoFocusImplementation();
127+
if (enableNewAutoFocusImpl) {
128+
[_viewController reactRemoveViewControllerAppearanceListener:self];
129+
_viewController = self.window ? [self reactViewController] : nil;
130+
[_viewController reactAddViewControllerAppearanceListener:self];
131+
}
132+
120133
if (self.window && !_didMoveToWindow) {
121-
const auto &props = static_cast<const TextInputProps &>(*_props);
122-
if (props.autoFocus) {
123-
[_backedTextInputView becomeFirstResponder];
124-
[self scrollCursorIntoView];
125-
}
126134
_didMoveToWindow = YES;
135+
if (!enableNewAutoFocusImpl) {
136+
[self tryAutoFocus];
137+
}
127138
[self initializeReturnKeyType];
128139
}
129140

@@ -378,9 +389,29 @@ - (void)prepareForRecycle
378389
_lastStringStateWasUpdatedWith = nil;
379390
_ignoreNextTextInputCall = NO;
380391
_didMoveToWindow = NO;
392+
_didAutoFocus = NO;
393+
[_viewController reactRemoveViewControllerAppearanceListener:self];
394+
_viewController = nil;
381395
[_backedTextInputView resignFirstResponder];
382396
}
383397

398+
#pragma mark - Auto focus / RCTViewControllerAppearanceListener
399+
400+
- (void)tryAutoFocus
401+
{
402+
const auto &props = static_cast<const TextInputProps &>(*_props);
403+
if (props.autoFocus && !_didAutoFocus) {
404+
_didAutoFocus = YES;
405+
[_backedTextInputView becomeFirstResponder];
406+
[self scrollCursorIntoView];
407+
}
408+
}
409+
410+
- (void)reactViewControllerDidAppear:(UIViewController *)viewController animated:(BOOL)animated
411+
{
412+
[self tryAutoFocus];
413+
}
414+
384415
#pragma mark - RCTBackedTextInputDelegate
385416

386417
- (BOOL)textInputShouldBeginEditing
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
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+
9+
#import <UIKit/UIKit.h>
10+
11+
NS_ASSUME_NONNULL_BEGIN
12+
13+
@interface RCTViewController : UIViewController
14+
15+
@end
16+
17+
NS_ASSUME_NONNULL_END
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// RCTViewController.m
3+
// React-Core
4+
//
5+
// Created by Hanno Goedecke on 27.04.26.
6+
//
7+
8+
#import "RCTViewController.h"
9+
#import <React/UIViewController+React.h>
10+
11+
@interface RCTViewController ()
12+
13+
@end
14+
15+
@implementation RCTViewController
16+
17+
- (void)viewDidAppear:(BOOL)animated
18+
{
19+
[super viewDidAppear:animated];
20+
[self reactNotifyViewControllerDidAppear:animated];
21+
}
22+
23+
- (void)viewDidDisappear:(BOOL)animated
24+
{
25+
[super viewDidDisappear:animated];
26+
[self reactNotifyViewControllerDidDisappear:animated];
27+
}
28+
29+
@end

packages/react-native/React/Views/RCTWrapperViewController.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
#import <UIKit/UIKit.h>
8+
#import <React/RCTViewController.h>
99

1010
@class RCTWrapperViewController;
1111

12-
@interface RCTWrapperViewController : UIViewController
12+
@interface RCTWrapperViewController : RCTViewController
1313

1414
- (instancetype)initWithContentView:(UIView *)contentView NS_DESIGNATED_INITIALIZER;
1515

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
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 <UIKit/UIKit.h>
9+
10+
NS_ASSUME_NONNULL_BEGIN
11+
12+
@protocol RCTViewControllerAppearanceListener <NSObject>
13+
14+
@optional
15+
- (void)reactViewControllerDidAppear:(UIViewController *)viewController animated:(BOOL)animated;
16+
- (void)reactViewControllerDidDisappear:(UIViewController *)viewController animated:(BOOL)animated;
17+
18+
@end
19+
20+
@interface UIViewController (React)
21+
22+
@property (nonatomic, assign, readonly) BOOL reactViewControllerIsVisible;
23+
24+
- (void)reactAddViewControllerAppearanceListener:(id<RCTViewControllerAppearanceListener>)listener;
25+
- (void)reactRemoveViewControllerAppearanceListener:(id<RCTViewControllerAppearanceListener>)listener;
26+
27+
/**
28+
* Call from `viewDidAppear:` / `viewDidDisappear:` in UIViewController subclasses
29+
* that want to notify registered React Native appearance listeners.
30+
*/
31+
- (void)reactNotifyViewControllerDidAppear:(BOOL)animated;
32+
- (void)reactNotifyViewControllerDidDisappear:(BOOL)animated;
33+
34+
@end
35+
36+
NS_ASSUME_NONNULL_END

0 commit comments

Comments
 (0)