From 3b90fd6e3877f154e68ba4112b38295365b57b84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 24 Apr 2026 16:08:09 +0200 Subject: [PATCH 1/5] feat(ios): RCTViewControllerAppearanceListener --- .../RCTDefaultReactNativeFactoryDelegate.mm | 3 +- .../Modal/RCTFabricModalHostViewController.h | 4 +- .../React/Views/RCTModalHostViewController.h | 4 +- .../React/Views/RCTViewController.h | 42 +++++++ .../React/Views/RCTViewController.m | 105 ++++++++++++++++++ .../React/Views/RCTWrapperViewController.h | 4 +- .../ios-prebuild/templates/React-umbrella.h | 1 + 7 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 packages/react-native/React/Views/RCTViewController.h create mode 100644 packages/react-native/React/Views/RCTViewController.m diff --git a/packages/react-native/Libraries/AppDelegate/RCTDefaultReactNativeFactoryDelegate.mm b/packages/react-native/Libraries/AppDelegate/RCTDefaultReactNativeFactoryDelegate.mm index 725c37baf52b..9ef4f13a88ad 100644 --- a/packages/react-native/Libraries/AppDelegate/RCTDefaultReactNativeFactoryDelegate.mm +++ b/packages/react-native/Libraries/AppDelegate/RCTDefaultReactNativeFactoryDelegate.mm @@ -7,6 +7,7 @@ #import "RCTDefaultReactNativeFactoryDelegate.h" #import +#import #import "RCTAppSetupUtils.h" #import "RCTDependencyProvider.h" #if USE_THIRD_PARTY_JSC != 1 @@ -28,7 +29,7 @@ - (NSURL *_Nullable)sourceURLForBridge:(nonnull RCTBridge *)bridge - (UIViewController *)createRootViewController { - return [UIViewController new]; + return [RCTViewController new]; } - (RCTBridge *)createBridgeWithDelegate:(id)delegate launchOptions:(NSDictionary *)launchOptions diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.h index 149a9788e2c1..059b415d0162 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.h @@ -5,13 +5,13 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import @protocol RCTFabricModalHostViewControllerDelegate - (void)boundsDidChange:(CGRect)newBounds; @end -@interface RCTFabricModalHostViewController : UIViewController +@interface RCTFabricModalHostViewController : RCTViewController @property (nonatomic, weak) id delegate; diff --git a/packages/react-native/React/Views/RCTModalHostViewController.h b/packages/react-native/React/Views/RCTModalHostViewController.h index 80b075045e5f..dd8f0474a69d 100644 --- a/packages/react-native/React/Views/RCTModalHostViewController.h +++ b/packages/react-native/React/Views/RCTModalHostViewController.h @@ -5,12 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import #ifndef RCT_REMOVE_LEGACY_ARCH __attribute__((deprecated("This API will be removed along with the legacy architecture."))) -@interface RCTModalHostViewController : UIViewController +@interface RCTModalHostViewController : RCTViewController @property (nonatomic, copy) void (^boundsDidChangeBlock)(CGRect newBounds); diff --git a/packages/react-native/React/Views/RCTViewController.h b/packages/react-native/React/Views/RCTViewController.h new file mode 100644 index 000000000000..8cd4d6a9f69f --- /dev/null +++ b/packages/react-native/React/Views/RCTViewController.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol RCTViewControllerAppearanceListener + +@optional +- (void)reactViewControllerDidAppear:(UIViewController *)viewController animated:(BOOL)animated; +- (void)reactViewControllerDidDisappear:(UIViewController *)viewController animated:(BOOL)animated; + +@end + +@interface UIViewController (RCTViewControllerAppearance) + +@property (nonatomic, assign, readonly) BOOL reactViewControllerIsVisible; + +- (void)reactAddViewControllerAppearanceListener:(id)listener; +- (void)reactRemoveViewControllerAppearanceListener:(id)listener; + +/** + * Call from `viewDidAppear:` / `viewDidDisappear:` in UIViewController subclasses + * that cannot inherit from RCTViewController. + */ +- (void)reactNotifyViewControllerDidAppear:(BOOL)animated; +- (void)reactNotifyViewControllerDidDisappear:(BOOL)animated; + +@end + +/** + * UIViewController subclass that forwards appearance events to registered listeners. + */ +@interface RCTViewController : UIViewController +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Views/RCTViewController.m b/packages/react-native/React/Views/RCTViewController.m new file mode 100644 index 000000000000..753bc10b1866 --- /dev/null +++ b/packages/react-native/React/Views/RCTViewController.m @@ -0,0 +1,105 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTViewController.h" + +#import + +@interface RCTViewControllerAppearanceState : NSObject + +@property (nonatomic, strong, readonly) NSHashTable> *listeners; +@property (nonatomic, assign) BOOL visible; + +@end + +@implementation RCTViewControllerAppearanceState + +- (instancetype)init +{ + if (self = [super init]) { + _listeners = [NSHashTable weakObjectsHashTable]; + } + return self; +} + +@end + +@implementation UIViewController (RCTViewControllerAppearance) + +- (RCTViewControllerAppearanceState *)reactViewControllerAppearanceState +{ + RCTViewControllerAppearanceState *state = + objc_getAssociatedObject(self, @selector(reactViewControllerAppearanceState)); + if (!state) { + state = [RCTViewControllerAppearanceState new]; + objc_setAssociatedObject( + self, @selector(reactViewControllerAppearanceState), state, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return state; +} + +- (BOOL)reactViewControllerIsVisible +{ + return [self reactViewControllerAppearanceState].visible; +} + +- (void)reactAddViewControllerAppearanceListener:(id)listener +{ + RCTViewControllerAppearanceState *state = [self reactViewControllerAppearanceState]; + [state.listeners addObject:listener]; + + if (state.visible && [listener respondsToSelector:@selector(reactViewControllerDidAppear:animated:)]) { + [listener reactViewControllerDidAppear:self animated:NO]; + } +} + +- (void)reactRemoveViewControllerAppearanceListener:(id)listener +{ + [[self reactViewControllerAppearanceState].listeners removeObject:listener]; +} + +- (void)reactNotifyViewControllerDidAppear:(BOOL)animated +{ + RCTViewControllerAppearanceState *state = [self reactViewControllerAppearanceState]; + state.visible = YES; + + for (id listener in state.listeners.allObjects) { + if ([listener respondsToSelector:@selector(reactViewControllerDidAppear:animated:)]) { + [listener reactViewControllerDidAppear:self animated:animated]; + } + } +} + +- (void)reactNotifyViewControllerDidDisappear:(BOOL)animated +{ + RCTViewControllerAppearanceState *state = [self reactViewControllerAppearanceState]; + state.visible = NO; + + for (id listener in state.listeners.allObjects) { + if ([listener respondsToSelector:@selector(reactViewControllerDidDisappear:animated:)]) { + [listener reactViewControllerDidDisappear:self animated:animated]; + } + } +} + +@end + +@implementation RCTViewController + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self reactNotifyViewControllerDidAppear:animated]; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self reactNotifyViewControllerDidDisappear:animated]; +} + +@end diff --git a/packages/react-native/React/Views/RCTWrapperViewController.h b/packages/react-native/React/Views/RCTWrapperViewController.h index b8277587684b..08100afb0d58 100644 --- a/packages/react-native/React/Views/RCTWrapperViewController.h +++ b/packages/react-native/React/Views/RCTWrapperViewController.h @@ -5,11 +5,11 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import @class RCTWrapperViewController; -@interface RCTWrapperViewController : UIViewController +@interface RCTWrapperViewController : RCTViewController - (instancetype)initWithContentView:(UIView *)contentView NS_DESIGNATED_INITIALIZER; diff --git a/packages/react-native/scripts/ios-prebuild/templates/React-umbrella.h b/packages/react-native/scripts/ios-prebuild/templates/React-umbrella.h index bcd298fd26f5..8b0b0fc1e707 100644 --- a/packages/react-native/scripts/ios-prebuild/templates/React-umbrella.h +++ b/packages/react-native/scripts/ios-prebuild/templates/React-umbrella.h @@ -274,6 +274,7 @@ #import #import #import +#import #import #import #import From 6d271eb6a8671efb9e1972046c513db13699ae24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 24 Apr 2026 16:28:33 +0200 Subject: [PATCH 2/5] simplify --- .../React/Views/RCTViewController.m | 53 +++++++------------ 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/packages/react-native/React/Views/RCTViewController.m b/packages/react-native/React/Views/RCTViewController.m index 753bc10b1866..adeec052ee72 100644 --- a/packages/react-native/React/Views/RCTViewController.m +++ b/packages/react-native/React/Views/RCTViewController.m @@ -9,65 +9,51 @@ #import -@interface RCTViewControllerAppearanceState : NSObject - -@property (nonatomic, strong, readonly) NSHashTable> *listeners; -@property (nonatomic, assign) BOOL visible; - -@end - -@implementation RCTViewControllerAppearanceState - -- (instancetype)init +static void RCTSetViewControllerIsVisible(UIViewController *viewController, BOOL isVisible) { - if (self = [super init]) { - _listeners = [NSHashTable weakObjectsHashTable]; - } - return self; + objc_setAssociatedObject( + viewController, @selector(reactViewControllerIsVisible), @(isVisible), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } -@end - @implementation UIViewController (RCTViewControllerAppearance) -- (RCTViewControllerAppearanceState *)reactViewControllerAppearanceState +- (NSHashTable> *)reactViewControllerAppearanceListeners { - RCTViewControllerAppearanceState *state = - objc_getAssociatedObject(self, @selector(reactViewControllerAppearanceState)); - if (!state) { - state = [RCTViewControllerAppearanceState new]; + NSHashTable> *listeners = + objc_getAssociatedObject(self, @selector(reactViewControllerAppearanceListeners)); + if (!listeners) { + listeners = [NSHashTable weakObjectsHashTable]; objc_setAssociatedObject( - self, @selector(reactViewControllerAppearanceState), state, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + self, @selector(reactViewControllerAppearanceListeners), listeners, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - return state; + return listeners; } - (BOOL)reactViewControllerIsVisible { - return [self reactViewControllerAppearanceState].visible; + return [objc_getAssociatedObject(self, @selector(reactViewControllerIsVisible)) boolValue]; } - (void)reactAddViewControllerAppearanceListener:(id)listener { - RCTViewControllerAppearanceState *state = [self reactViewControllerAppearanceState]; - [state.listeners addObject:listener]; + [[self reactViewControllerAppearanceListeners] addObject:listener]; - if (state.visible && [listener respondsToSelector:@selector(reactViewControllerDidAppear:animated:)]) { + if (self.reactViewControllerIsVisible && + [listener respondsToSelector:@selector(reactViewControllerDidAppear:animated:)]) { [listener reactViewControllerDidAppear:self animated:NO]; } } - (void)reactRemoveViewControllerAppearanceListener:(id)listener { - [[self reactViewControllerAppearanceState].listeners removeObject:listener]; + [[self reactViewControllerAppearanceListeners] removeObject:listener]; } - (void)reactNotifyViewControllerDidAppear:(BOOL)animated { - RCTViewControllerAppearanceState *state = [self reactViewControllerAppearanceState]; - state.visible = YES; + RCTSetViewControllerIsVisible(self, YES); - for (id listener in state.listeners.allObjects) { + for (id listener in [self reactViewControllerAppearanceListeners].allObjects) { if ([listener respondsToSelector:@selector(reactViewControllerDidAppear:animated:)]) { [listener reactViewControllerDidAppear:self animated:animated]; } @@ -76,10 +62,9 @@ - (void)reactNotifyViewControllerDidAppear:(BOOL)animated - (void)reactNotifyViewControllerDidDisappear:(BOOL)animated { - RCTViewControllerAppearanceState *state = [self reactViewControllerAppearanceState]; - state.visible = NO; + RCTSetViewControllerIsVisible(self, NO); - for (id listener in state.listeners.allObjects) { + for (id listener in [self reactViewControllerAppearanceListeners].allObjects) { if ([listener respondsToSelector:@selector(reactViewControllerDidDisappear:animated:)]) { [listener reactViewControllerDidDisappear:self animated:animated]; } From f42594f69335c5c7d3f2f490b03d56350b63a837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 24 Apr 2026 16:59:14 +0200 Subject: [PATCH 3/5] fix API mismatches --- .../api-snapshots/ReactAppleDebugCxx.api | 22 ++++++++++++++++--- .../api-snapshots/ReactAppleReleaseCxx.api | 22 ++++++++++++++++--- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index f2c8329be9a1..3ca77e5a2ac3 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -325,6 +325,14 @@ category UIView(React) { public virtual void removeReactSubview:(UIView* subview); } +category UIViewController(RCTViewControllerAppearance) { + public @property (assign, readonly) BOOL reactViewControllerIsVisible; + public virtual void reactAddViewControllerAppearanceListener:(id listener); + public virtual void reactNotifyViewControllerDidAppear:(BOOL animated); + public virtual void reactNotifyViewControllerDidDisappear:(BOOL animated); + public virtual void reactRemoveViewControllerAppearanceListener:(id listener); +} + class ObjCTimerRegistry : public facebook::react::PlatformTimerRegistry { public ObjCTimerRegistry(); public RCTTiming* _Null_unspecified timing; @@ -1156,7 +1164,7 @@ interface RCTExceptionsManager : public NSObject { public virtual void reportSoftException:stack:exceptionId:(_Nullable NSString* message, _Nullable NSArray* stack, double exceptionId); } -interface RCTFabricModalHostViewController : public UIViewController { +interface RCTFabricModalHostViewController : public RCTViewController { public @property (assign) UIInterfaceOrientationMask supportedInterfaceOrientations; public @property (weak) id delegate; } @@ -1517,7 +1525,7 @@ interface RCTModalHostViewComponentView : public RCTViewComponentView { public @property (weak) RCTBridge* bridge; public CGFloat RCTJSONParseOnlyNumber(id json); @@ -2530,7 +2541,7 @@ interface RCTWrapperView : public UIView { public virtual instancetype initWithBridge:(RCTBridge* bridge); } -interface RCTWrapperViewController : public UIViewController { +interface RCTWrapperViewController : public RCTViewController { public virtual instancetype initWithContentView:(UIView* contentView); } @@ -3493,6 +3504,11 @@ protocol RCTValueAnimatedNodeObserver : public NSObject { public virtual void animatedNode:didUpdateValue:(RCTValueAnimatedNode* node, CGFloat value); } +protocol RCTViewControllerAppearanceListener : public NSObject { + public virtual void reactViewControllerDidAppear:animated:(UIViewController* viewController, BOOL animated); + public virtual void reactViewControllerDidDisappear:animated:(UIViewController* viewController, BOOL animated); +} + protocol RCTVirtualViewContainerProtocol { public virtual RCTVirtualViewContainerState* virtualViewContainerState(); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index b7e4ab668f81..4a90f9ef5f13 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -325,6 +325,14 @@ category UIView(React) { public virtual void removeReactSubview:(UIView* subview); } +category UIViewController(RCTViewControllerAppearance) { + public @property (assign, readonly) BOOL reactViewControllerIsVisible; + public virtual void reactAddViewControllerAppearanceListener:(id listener); + public virtual void reactNotifyViewControllerDidAppear:(BOOL animated); + public virtual void reactNotifyViewControllerDidDisappear:(BOOL animated); + public virtual void reactRemoveViewControllerAppearanceListener:(id listener); +} + class ObjCTimerRegistry : public facebook::react::PlatformTimerRegistry { public ObjCTimerRegistry(); public RCTTiming* _Null_unspecified timing; @@ -1156,7 +1164,7 @@ interface RCTExceptionsManager : public NSObject { public virtual void reportSoftException:stack:exceptionId:(_Nullable NSString* message, _Nullable NSArray* stack, double exceptionId); } -interface RCTFabricModalHostViewController : public UIViewController { +interface RCTFabricModalHostViewController : public RCTViewController { public @property (assign) UIInterfaceOrientationMask supportedInterfaceOrientations; public @property (weak) id delegate; } @@ -1517,7 +1525,7 @@ interface RCTModalHostViewComponentView : public RCTViewComponentView { public @property (weak) RCTBridge* bridge; public CGFloat RCTJSONParseOnlyNumber(id json); @@ -2530,7 +2541,7 @@ interface RCTWrapperView : public UIView { public virtual instancetype initWithBridge:(RCTBridge* bridge); } -interface RCTWrapperViewController : public UIViewController { +interface RCTWrapperViewController : public RCTViewController { public virtual instancetype initWithContentView:(UIView* contentView); } @@ -3493,6 +3504,11 @@ protocol RCTValueAnimatedNodeObserver : public NSObject { public virtual void animatedNode:didUpdateValue:(RCTValueAnimatedNode* node, CGFloat value); } +protocol RCTViewControllerAppearanceListener : public NSObject { + public virtual void reactViewControllerDidAppear:animated:(UIViewController* viewController, BOOL animated); + public virtual void reactViewControllerDidDisappear:animated:(UIViewController* viewController, BOOL animated); +} + protocol RCTVirtualViewContainerProtocol { public virtual RCTVirtualViewContainerState* virtualViewContainerState(); } From 94ad872301af6c61ac076bc601297f3f045ea6ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 24 Apr 2026 17:03:45 +0200 Subject: [PATCH 4/5] remove RCTViewController --- .../RCTDefaultReactNativeFactoryDelegate.mm | 3 +- .../Modal/RCTFabricModalHostViewController.h | 4 +- .../React/Views/RCTModalHostViewController.h | 4 +- .../React/Views/RCTViewController.m | 90 ------------------- .../React/Views/RCTWrapperViewController.h | 4 +- ...wController.h => UIViewController+React.h} | 10 +-- .../React/Views/UIViewController+React.m | 89 ++++++++++++++++++ .../ios-prebuild/templates/React-umbrella.h | 2 +- 8 files changed, 99 insertions(+), 107 deletions(-) delete mode 100644 packages/react-native/React/Views/RCTViewController.m rename packages/react-native/React/Views/{RCTViewController.h => UIViewController+React.h} (80%) create mode 100644 packages/react-native/React/Views/UIViewController+React.m diff --git a/packages/react-native/Libraries/AppDelegate/RCTDefaultReactNativeFactoryDelegate.mm b/packages/react-native/Libraries/AppDelegate/RCTDefaultReactNativeFactoryDelegate.mm index 9ef4f13a88ad..725c37baf52b 100644 --- a/packages/react-native/Libraries/AppDelegate/RCTDefaultReactNativeFactoryDelegate.mm +++ b/packages/react-native/Libraries/AppDelegate/RCTDefaultReactNativeFactoryDelegate.mm @@ -7,7 +7,6 @@ #import "RCTDefaultReactNativeFactoryDelegate.h" #import -#import #import "RCTAppSetupUtils.h" #import "RCTDependencyProvider.h" #if USE_THIRD_PARTY_JSC != 1 @@ -29,7 +28,7 @@ - (NSURL *_Nullable)sourceURLForBridge:(nonnull RCTBridge *)bridge - (UIViewController *)createRootViewController { - return [RCTViewController new]; + return [UIViewController new]; } - (RCTBridge *)createBridgeWithDelegate:(id)delegate launchOptions:(NSDictionary *)launchOptions diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.h index 059b415d0162..149a9788e2c1 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.h @@ -5,13 +5,13 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import @protocol RCTFabricModalHostViewControllerDelegate - (void)boundsDidChange:(CGRect)newBounds; @end -@interface RCTFabricModalHostViewController : RCTViewController +@interface RCTFabricModalHostViewController : UIViewController @property (nonatomic, weak) id delegate; diff --git a/packages/react-native/React/Views/RCTModalHostViewController.h b/packages/react-native/React/Views/RCTModalHostViewController.h index dd8f0474a69d..80b075045e5f 100644 --- a/packages/react-native/React/Views/RCTModalHostViewController.h +++ b/packages/react-native/React/Views/RCTModalHostViewController.h @@ -5,12 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import #ifndef RCT_REMOVE_LEGACY_ARCH __attribute__((deprecated("This API will be removed along with the legacy architecture."))) -@interface RCTModalHostViewController : RCTViewController +@interface RCTModalHostViewController : UIViewController @property (nonatomic, copy) void (^boundsDidChangeBlock)(CGRect newBounds); diff --git a/packages/react-native/React/Views/RCTViewController.m b/packages/react-native/React/Views/RCTViewController.m deleted file mode 100644 index adeec052ee72..000000000000 --- a/packages/react-native/React/Views/RCTViewController.m +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import "RCTViewController.h" - -#import - -static void RCTSetViewControllerIsVisible(UIViewController *viewController, BOOL isVisible) -{ - objc_setAssociatedObject( - viewController, @selector(reactViewControllerIsVisible), @(isVisible), OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -@implementation UIViewController (RCTViewControllerAppearance) - -- (NSHashTable> *)reactViewControllerAppearanceListeners -{ - NSHashTable> *listeners = - objc_getAssociatedObject(self, @selector(reactViewControllerAppearanceListeners)); - if (!listeners) { - listeners = [NSHashTable weakObjectsHashTable]; - objc_setAssociatedObject( - self, @selector(reactViewControllerAppearanceListeners), listeners, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - } - return listeners; -} - -- (BOOL)reactViewControllerIsVisible -{ - return [objc_getAssociatedObject(self, @selector(reactViewControllerIsVisible)) boolValue]; -} - -- (void)reactAddViewControllerAppearanceListener:(id)listener -{ - [[self reactViewControllerAppearanceListeners] addObject:listener]; - - if (self.reactViewControllerIsVisible && - [listener respondsToSelector:@selector(reactViewControllerDidAppear:animated:)]) { - [listener reactViewControllerDidAppear:self animated:NO]; - } -} - -- (void)reactRemoveViewControllerAppearanceListener:(id)listener -{ - [[self reactViewControllerAppearanceListeners] removeObject:listener]; -} - -- (void)reactNotifyViewControllerDidAppear:(BOOL)animated -{ - RCTSetViewControllerIsVisible(self, YES); - - for (id listener in [self reactViewControllerAppearanceListeners].allObjects) { - if ([listener respondsToSelector:@selector(reactViewControllerDidAppear:animated:)]) { - [listener reactViewControllerDidAppear:self animated:animated]; - } - } -} - -- (void)reactNotifyViewControllerDidDisappear:(BOOL)animated -{ - RCTSetViewControllerIsVisible(self, NO); - - for (id listener in [self reactViewControllerAppearanceListeners].allObjects) { - if ([listener respondsToSelector:@selector(reactViewControllerDidDisappear:animated:)]) { - [listener reactViewControllerDidDisappear:self animated:animated]; - } - } -} - -@end - -@implementation RCTViewController - -- (void)viewDidAppear:(BOOL)animated -{ - [super viewDidAppear:animated]; - [self reactNotifyViewControllerDidAppear:animated]; -} - -- (void)viewDidDisappear:(BOOL)animated -{ - [super viewDidDisappear:animated]; - [self reactNotifyViewControllerDidDisappear:animated]; -} - -@end diff --git a/packages/react-native/React/Views/RCTWrapperViewController.h b/packages/react-native/React/Views/RCTWrapperViewController.h index 08100afb0d58..b8277587684b 100644 --- a/packages/react-native/React/Views/RCTWrapperViewController.h +++ b/packages/react-native/React/Views/RCTWrapperViewController.h @@ -5,11 +5,11 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import @class RCTWrapperViewController; -@interface RCTWrapperViewController : RCTViewController +@interface RCTWrapperViewController : UIViewController - (instancetype)initWithContentView:(UIView *)contentView NS_DESIGNATED_INITIALIZER; diff --git a/packages/react-native/React/Views/RCTViewController.h b/packages/react-native/React/Views/UIViewController+React.h similarity index 80% rename from packages/react-native/React/Views/RCTViewController.h rename to packages/react-native/React/Views/UIViewController+React.h index 8cd4d6a9f69f..f765467a0223 100644 --- a/packages/react-native/React/Views/RCTViewController.h +++ b/packages/react-native/React/Views/UIViewController+React.h @@ -17,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN @end -@interface UIViewController (RCTViewControllerAppearance) +@interface UIViewController (React) @property (nonatomic, assign, readonly) BOOL reactViewControllerIsVisible; @@ -26,17 +26,11 @@ NS_ASSUME_NONNULL_BEGIN /** * Call from `viewDidAppear:` / `viewDidDisappear:` in UIViewController subclasses - * that cannot inherit from RCTViewController. + * that want to notify registered React Native appearance listeners. */ - (void)reactNotifyViewControllerDidAppear:(BOOL)animated; - (void)reactNotifyViewControllerDidDisappear:(BOOL)animated; @end -/** - * UIViewController subclass that forwards appearance events to registered listeners. - */ -@interface RCTViewController : UIViewController -@end - NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Views/UIViewController+React.m b/packages/react-native/React/Views/UIViewController+React.m new file mode 100644 index 000000000000..df18a0904051 --- /dev/null +++ b/packages/react-native/React/Views/UIViewController+React.m @@ -0,0 +1,89 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "UIViewController+React.h" + +#import + +@interface RCTViewControllerAppearanceState : NSObject + +@property (nonatomic, strong, readonly) NSHashTable> *listeners; +@property (nonatomic, assign) BOOL visible; + +@end + +@implementation RCTViewControllerAppearanceState + +- (instancetype)init +{ + if (self = [super init]) { + _listeners = [NSHashTable weakObjectsHashTable]; + } + return self; +} + +@end + +@implementation UIViewController (React) + +- (RCTViewControllerAppearanceState *)reactViewControllerAppearanceState +{ + RCTViewControllerAppearanceState *state = + objc_getAssociatedObject(self, @selector(reactViewControllerAppearanceState)); + if (!state) { + state = [RCTViewControllerAppearanceState new]; + objc_setAssociatedObject( + self, @selector(reactViewControllerAppearanceState), state, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return state; +} + +- (BOOL)reactViewControllerIsVisible +{ + return [self reactViewControllerAppearanceState].visible; +} + +- (void)reactAddViewControllerAppearanceListener:(id)listener +{ + RCTViewControllerAppearanceState *state = [self reactViewControllerAppearanceState]; + [state.listeners addObject:listener]; + + if (state.visible && [listener respondsToSelector:@selector(reactViewControllerDidAppear:animated:)]) { + [listener reactViewControllerDidAppear:self animated:NO]; + } +} + +- (void)reactRemoveViewControllerAppearanceListener:(id)listener +{ + [[self reactViewControllerAppearanceState].listeners removeObject:listener]; +} + +- (void)reactNotifyViewControllerDidAppear:(BOOL)animated +{ + RCTViewControllerAppearanceState *state = [self reactViewControllerAppearanceState]; + state.visible = YES; + + for (id listener in state.listeners.allObjects) { + if ([listener respondsToSelector:@selector(reactViewControllerDidAppear:animated:)]) { + [listener reactViewControllerDidAppear:self animated:animated]; + } + } +} + +- (void)reactNotifyViewControllerDidDisappear:(BOOL)animated +{ + RCTViewControllerAppearanceState *state = [self reactViewControllerAppearanceState]; + state.visible = NO; + + for (id listener in state.listeners.allObjects) { + if ([listener respondsToSelector:@selector(reactViewControllerDidDisappear:animated:)]) { + [listener reactViewControllerDidDisappear:self animated:animated]; + } + } +} + +@end diff --git a/packages/react-native/scripts/ios-prebuild/templates/React-umbrella.h b/packages/react-native/scripts/ios-prebuild/templates/React-umbrella.h index 8b0b0fc1e707..19898532a7b6 100644 --- a/packages/react-native/scripts/ios-prebuild/templates/React-umbrella.h +++ b/packages/react-native/scripts/ios-prebuild/templates/React-umbrella.h @@ -274,7 +274,6 @@ #import #import #import -#import #import #import #import @@ -284,6 +283,7 @@ #import #import #import +#import FOUNDATION_EXPORT double ReactVersionNumber; FOUNDATION_EXPORT const unsigned char ReactVersionString[]; From 3c03d8e7ad36fc8d062616535452e53642ab954c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 24 Apr 2026 17:10:25 +0200 Subject: [PATCH 5/5] fix API mismatches --- scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api | 11 ++++------- .../cxx-api/api-snapshots/ReactAppleReleaseCxx.api | 11 ++++------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 3ca77e5a2ac3..a6d24b8d3c46 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -325,7 +325,7 @@ category UIView(React) { public virtual void removeReactSubview:(UIView* subview); } -category UIViewController(RCTViewControllerAppearance) { +category UIViewController(React) { public @property (assign, readonly) BOOL reactViewControllerIsVisible; public virtual void reactAddViewControllerAppearanceListener:(id listener); public virtual void reactNotifyViewControllerDidAppear:(BOOL animated); @@ -1164,7 +1164,7 @@ interface RCTExceptionsManager : public NSObject { public virtual void reportSoftException:stack:exceptionId:(_Nullable NSString* message, _Nullable NSArray* stack, double exceptionId); } -interface RCTFabricModalHostViewController : public RCTViewController { +interface RCTFabricModalHostViewController : public UIViewController { public @property (assign) UIInterfaceOrientationMask supportedInterfaceOrientations; public @property (weak) id delegate; } @@ -1525,7 +1525,7 @@ interface RCTModalHostViewComponentView : public RCTViewComponentView { public @property (weak) RCTBridge* bridge; public CGFloat RCTJSONParseOnlyNumber(id json); @@ -2541,7 +2538,7 @@ interface RCTWrapperView : public UIView { public virtual instancetype initWithBridge:(RCTBridge* bridge); } -interface RCTWrapperViewController : public RCTViewController { +interface RCTWrapperViewController : public UIViewController { public virtual instancetype initWithContentView:(UIView* contentView); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 4a90f9ef5f13..5a12845514e5 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -325,7 +325,7 @@ category UIView(React) { public virtual void removeReactSubview:(UIView* subview); } -category UIViewController(RCTViewControllerAppearance) { +category UIViewController(React) { public @property (assign, readonly) BOOL reactViewControllerIsVisible; public virtual void reactAddViewControllerAppearanceListener:(id listener); public virtual void reactNotifyViewControllerDidAppear:(BOOL animated); @@ -1164,7 +1164,7 @@ interface RCTExceptionsManager : public NSObject { public virtual void reportSoftException:stack:exceptionId:(_Nullable NSString* message, _Nullable NSArray* stack, double exceptionId); } -interface RCTFabricModalHostViewController : public RCTViewController { +interface RCTFabricModalHostViewController : public UIViewController { public @property (assign) UIInterfaceOrientationMask supportedInterfaceOrientations; public @property (weak) id delegate; } @@ -1525,7 +1525,7 @@ interface RCTModalHostViewComponentView : public RCTViewComponentView { public @property (weak) RCTBridge* bridge; public CGFloat RCTJSONParseOnlyNumber(id json); @@ -2541,7 +2538,7 @@ interface RCTWrapperView : public UIView { public virtual instancetype initWithBridge:(RCTBridge* bridge); } -interface RCTWrapperViewController : public RCTViewController { +interface RCTWrapperViewController : public UIViewController { public virtual instancetype initWithContentView:(UIView* contentView); }