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/Views/RCTViewController.h b/packages/react-native/React/Views/RCTViewController.h new file mode 100644 index 000000000000..020630862dec --- /dev/null +++ b/packages/react-native/React/Views/RCTViewController.h @@ -0,0 +1,17 @@ +/* + * 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 + +@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..c9d111b3e17a --- /dev/null +++ b/packages/react-native/React/Views/RCTViewController.m @@ -0,0 +1,29 @@ +// +// RCTViewController.m +// React-Core +// +// Created by Hanno Goedecke on 27.04.26. +// + +#import "RCTViewController.h" +#import + +@interface RCTViewController () + +@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/React/Views/UIViewController+React.h b/packages/react-native/React/Views/UIViewController+React.h new file mode 100644 index 000000000000..f765467a0223 --- /dev/null +++ b/packages/react-native/React/Views/UIViewController+React.h @@ -0,0 +1,36 @@ +/* + * 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 (React) + +@property (nonatomic, assign, readonly) BOOL reactViewControllerIsVisible; + +- (void)reactAddViewControllerAppearanceListener:(id)listener; +- (void)reactRemoveViewControllerAppearanceListener:(id)listener; + +/** + * Call from `viewDidAppear:` / `viewDidDisappear:` in UIViewController subclasses + * that want to notify registered React Native appearance listeners. + */ +- (void)reactNotifyViewControllerDidAppear:(BOOL)animated; +- (void)reactNotifyViewControllerDidDisappear:(BOOL)animated; + +@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 bcd298fd26f5..19898532a7b6 100644 --- a/packages/react-native/scripts/ios-prebuild/templates/React-umbrella.h +++ b/packages/react-native/scripts/ios-prebuild/templates/React-umbrella.h @@ -283,6 +283,7 @@ #import #import #import +#import FOUNDATION_EXPORT double ReactVersionNumber; FOUNDATION_EXPORT const unsigned char ReactVersionString[]; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index f2c8329be9a1..e8d381b3a7ca 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(React) { + 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; @@ -2467,6 +2475,9 @@ interface RCTViewComponentView : public UIView { 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..ac156f189fdc 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(React) { + 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; @@ -2467,6 +2475,9 @@ interface RCTViewComponentView : public UIView { 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(); }