Skip to content

feat(ios): RCTViewControllerAppearanceListener#56598

Open
hannojg wants to merge 5 commits intofacebook:mainfrom
hannojg:hannojg/feat-uiviewcontroller-viewdidappear-listener-pattern
Open

feat(ios): RCTViewControllerAppearanceListener#56598
hannojg wants to merge 5 commits intofacebook:mainfrom
hannojg:hannojg/feat-uiviewcontroller-viewdidappear-listener-pattern

Conversation

@hannojg
Copy link
Copy Markdown
Contributor

@hannojg hannojg commented Apr 24, 2026

Summary:

This is the first step to add support in react-native for views to listen to their parent UIViewController viewDidAppear: event.
Proposing this as part of:

Example

Some native view component potentially want to hook into the UIViewController's viewDidAppear(_:) event to run some logic (e.g. a TextInput may wants to apply its auto focus logic at this point).
For a UIViewController in react-native to support this it has to opt into it, by implementing viewDidAppear:

#import <React/UIViewController+React.h>

@implementation MyViewController

- (void)viewDidAppear:(BOOL)animated
{
  [super viewDidAppear:animated];
  [self reactNotifyViewControllerDidAppear:animated];
}

A view component interested in that event can then add a listener, which will be called (either in the future, or immediately if the VC already appeared):

#import <React/UIView+React.h>
#import <React/UIViewController+React.h>

@interface MyComponentView () <RCTViewControllerAppearanceListener>
@end

@implementation MyComponentView {
  __weak UIViewController *_viewController;
}

- (void)didMoveToWindow
{
  [super didMoveToWindow];

  [_viewController reactRemoveViewControllerAppearanceListener:self];
  _viewController = self.window ? [self reactViewController] : nil;
  [_viewController reactAddViewControllerAppearanceListener:self];
}

- (void)prepareForRecycle
{
  [_viewController reactRemoveViewControllerAppearanceListener:self];
  _viewController = nil;

  [super prepareForRecycle];
}

- (void)reactViewControllerDidAppear:(UIViewController *)viewController animated:(BOOL)animated
{
  // your viewDidAppear hook!
}

@end

For an example see also changing react-native's root controller: #56618

Changelog:

[IOS] [ADDED] - Add support to listen to UIViewController viewDidAppear: & viewDidDisappear:

Test Plan:

Test that RN tester is still compiling and working as usual

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Apr 24, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Apr 24, 2026
Copy link
Copy Markdown
Contributor

@cipolleschi cipolleschi left a comment

Choose a reason for hiding this comment

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

@hannojg Thanks for working on this, but unfortunately we can't merge this as it is. Adding an RCTViewController base class is a huge breaking change as it implies that all the view controller used by react native must inherit from the RCTViewController. It means that if some user customize the createRootViewController function by returning something different (eg. a UITTabController), they are going to lose all the features that you are pushing with RCTViewController.

We should probably find a better way to handle this that does not requires a new base class for ViewControllers.

@github-actions
Copy link
Copy Markdown

Warning

JavaScript API change detected

This PR commits an update to ReactNativeApi.d.ts, indicating a change to React Native's public JavaScript API.

  • Please include a clear changelog message.
  • This change will be subject to additional review.

This change was flagged as: POTENTIALLY_BREAKING

@hannojg
Copy link
Copy Markdown
Contributor Author

hannojg commented Apr 24, 2026

Hey @cipolleschi , okay, i see! I don't think this base class is needed. I added a category on UIViewController. This way you don't need to subclass the RCTViewController but if you want to opt into this behaviour you'd just implement:

@implementation MyViewController

- (void)viewDidAppear:(BOOL)animated
{
  [super viewDidAppear:animated];
  [self reactNotifyViewControllerDidAppear:animated];
}

how does that sound?
But yes, implementations that would want to support this properly would need to implement this...
For react-native-screens this is something i already contacted the authors about and they seem to be onboard with it.
I was also thinking how to do this automatically, and there might be a way with injecting an invisible view (or swizzling) but i found all of that very hacky, so I wanted to go for a opt-in API.

@hannojg hannojg requested a review from cipolleschi April 24, 2026 15:10
Copy link
Copy Markdown
Contributor

@WoLewicki WoLewicki left a comment

Choose a reason for hiding this comment

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

Just some comments for now, looks reasonable.

- (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

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

@cipolleschi
Copy link
Copy Markdown
Contributor

This is much better, thanks for working on this.
How are VC supposed to use this? If there is a follow-up PR, adds it in the summary. Otherwise, let's add an example in the summary.

@hannojg
Copy link
Copy Markdown
Contributor Author

hannojg commented Apr 27, 2026

I added an example into the summary, but also made a change to use this pattern for react-native's root view controller:

Not sure though how important it is for react-native core, as this only gets relevant if you use multiple view controllers.

I will now make two other PRs:

  • One that adds a feature flag for this experimental new focus approach (link)
  • One that implements listening to viewDidAppear in RCTTextInputComponentView (link)

That should give us the full picture of how all of that works together!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants