Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions packages/react-native/React/Views/UIViewController+React.h
Original file line number Diff line number Diff line change
@@ -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 <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@protocol RCTViewControllerAppearanceListener <NSObject>

@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<RCTViewControllerAppearanceListener>)listener;
- (void)reactRemoveViewControllerAppearanceListener:(id<RCTViewControllerAppearanceListener>)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
89 changes: 89 additions & 0 deletions packages/react-native/React/Views/UIViewController+React.m
Original file line number Diff line number Diff line change
@@ -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 <objc/runtime.h>

@interface RCTViewControllerAppearanceState : NSObject

@property (nonatomic, strong, readonly) NSHashTable<id<RCTViewControllerAppearanceListener>> *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));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're doing it in such weird way cause its category right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, unfortunately

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<RCTViewControllerAppearanceListener>)listener
{
RCTViewControllerAppearanceState *state = [self reactViewControllerAppearanceState];
[state.listeners addObject:listener];

if (state.visible && [listener respondsToSelector:@selector(reactViewControllerDidAppear:animated:)]) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to check the selector? Guess that if it does not implement it then it will just fail silently right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, why are we dispatching something in the registration method?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to check the selector?

Yes, or are we going to get not recognized selector error otherwise.

Also, why are we dispatching something in the registration method

Are you worried that we might not catch all the events?

Copy link
Copy Markdown
Contributor Author

@hannojg hannojg Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, we need the check, otherwise we would get unrecognized selector exception i think 🤔

why are we dispatching something in the registration method?

I think there are two ways to handle this with listener patterns. The question is basically, what should happen if you attach a listener for something that has already happened. Should the listener callback be invoked immediately (the case here), or is it the callers responsibility to check for the condition first (and its for future events only).
I felt like its easier to just send it here so the caller doesn't have to do extra checks + the event is view_Did_Appear, so I felt its reasonable to fire it when the event has already happened. Plus i think it will just make it simpler for the calling site

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Edit: oops, only saw Riccardo's comment now

[listener reactViewControllerDidAppear:self animated:NO];
}
}

- (void)reactRemoveViewControllerAppearanceListener:(id<RCTViewControllerAppearanceListener>)listener
{
[[self reactViewControllerAppearanceState].listeners removeObject:listener];
}

- (void)reactNotifyViewControllerDidAppear:(BOOL)animated
{
RCTViewControllerAppearanceState *state = [self reactViewControllerAppearanceState];
state.visible = YES;

for (id<RCTViewControllerAppearanceListener> 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<RCTViewControllerAppearanceListener> listener in state.listeners.allObjects) {
if ([listener respondsToSelector:@selector(reactViewControllerDidDisappear:animated:)]) {
[listener reactViewControllerDidDisappear:self animated:animated];
}
}
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@
#import <React/RCTWrapperViewController.h>
#import <React/UIView+Private.h>
#import <React/UIView+React.h>
#import <React/UIViewController+React.h>

FOUNDATION_EXPORT double ReactVersionNumber;
FOUNDATION_EXPORT const unsigned char ReactVersionString[];
13 changes: 13 additions & 0 deletions scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -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<RCTViewControllerAppearanceListener> listener);
public virtual void reactNotifyViewControllerDidAppear:(BOOL animated);
public virtual void reactNotifyViewControllerDidDisappear:(BOOL animated);
public virtual void reactRemoveViewControllerAppearanceListener:(id<RCTViewControllerAppearanceListener> listener);
}

class ObjCTimerRegistry : public facebook::react::PlatformTimerRegistry {
public ObjCTimerRegistry();
public RCTTiming* _Null_unspecified timing;
Expand Down Expand Up @@ -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();
}
Expand Down
13 changes: 13 additions & 0 deletions scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -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<RCTViewControllerAppearanceListener> listener);
public virtual void reactNotifyViewControllerDidAppear:(BOOL animated);
public virtual void reactNotifyViewControllerDidDisappear:(BOOL animated);
public virtual void reactRemoveViewControllerAppearanceListener:(id<RCTViewControllerAppearanceListener> listener);
}

class ObjCTimerRegistry : public facebook::react::PlatformTimerRegistry {
public ObjCTimerRegistry();
public RCTTiming* _Null_unspecified timing;
Expand Down Expand Up @@ -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();
}
Expand Down
Loading