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..a6d24b8d3c46 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; @@ -3493,6 +3501,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..5a12845514e5 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; @@ -3493,6 +3501,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(); }