react-native: iOS AppDelegate changes no longer permit configuring a loading view fade delay to avoid native -> JS "white flash"

New Version

0.71.1

Old Version

0.70.6

Build Target(s)

iOS

Output of react-native info

System: OS: macOS 13.2 CPU: (10) arm64 Apple M1 Pro Memory: 85.22 MB / 16.00 GB Shell: 5.8.1 - /bin/zsh Binaries: Node: 19.4.0 - /private/var/folders/lx/8pd1v4bj6656dm288zdsl9p00000gn/T/xfs-1926afa9/node Yarn: 3.3.1 - /private/var/folders/lx/8pd1v4bj6656dm288zdsl9p00000gn/T/xfs-1926afa9/yarn npm: 9.2.0 - /opt/homebrew/bin/npm Watchman: 2023.01.16.00 - /opt/homebrew/bin/watchman Managers: CocoaPods: 1.11.3 - /Users/craig/.rvm/gems/ruby-2.7.6/bin/pod SDKs: iOS SDK: Platforms: DriverKit 22.2, iOS 16.2, macOS 13.1, tvOS 16.1, watchOS 9.1 Android SDK: API Levels: 24, 25, 26, 27, 28, 29, 30, 31, 33 Build Tools: 28.0.3, 29.0.2, 29.0.3, 30.0.1, 30.0.2, 30.0.3, 31.0.0, 33.0.0 System Images: android-28 | Google Play Intel x86 Atom, android-29 | Google APIs Intel x86 Atom, android-29 | Google Play Intel x86 Atom, android-30 | Google APIs Intel x86 Atom, android-30 | Google APIs Intel x86 Atom_64, android-30 | Google Play Intel x86 Atom, android-30 | Google Play Intel x86 Atom_64 Android NDK: Not Found IDEs: Android Studio: 2021.3 AI-213.7172.25.2113.9123335 Xcode: 14.2/14C18 - /usr/bin/xcodebuild Languages: Java: 11.0.12 - /usr/bin/javac npmPackages: @react-native-community/cli: Not Found react: 18.2.0 => 18.2.0 react-native: 0.71.1 => 0.71.1 react-native-macos: Not Found npmGlobalPackages: react-native: Not Found

Issue and Reproduction Steps

First of all, thank you for all the hard work improving React Native! The changes made to both the Android and iOS setup configs are great (and welcomed) however we are running into a challenge on iOS. Previously we could write something like the following:

NSDictionary *initProps = [self prepareInitialProps];
RCTRootView *rootView = (RCTRootView *)RCTAppSetupDefaultRootView(bridge, @"ExampleApp", initProps);

if (@available(iOS 13.0, *)) {
  rootView.backgroundColor = [UIColor systemBackgroundColor];
} else {
  rootView.backgroundColor = [UIColor whiteColor];
}

self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];

UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"LaunchScreen" bundle:nil];
UIView *loadingView = [[storyboard instantiateInitialViewController] view];
[rootView setLoadingView:loadingView];
rootView.loadingViewFadeDelay = 0.5;
rootView.loadingViewFadeDuration = 0.5;

to, in the user’s eyes, delay the fade out of the launch screen to avoid the white flash between the native launch screen and our JS “splash” screen. This has been working well for countless releases. With the root view logic being internalized to the RN setup, it is not clear how to keep this behavior (if at all possible). From what I gather, the new architecture doesn’t have a mechanism to accomplish this either. So the ask is 1) expose some config variables for (existing architecture) to keep this functionality in place for those that currently use it 2) implement a way for the new architecture to have this same behavior (eventually the switch will be made).

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 8
  • Comments: 46 (23 by maintainers)

Commits related to this issue

Most upvoted comments

Hi @gtokman, you are right.

Currently, there is a bug in RN 0.71 for the backgroundColor only, as explained it here. Basically, you can customize the view, but then we will replace the background color. 🤦

This will be fixed in 0.72, but I can try to backport it to 0.71.

Meanwhile, what you can do is the following:

// In AppDelegate
 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  self.moduleName = @"InteropApp";
  // You can add your custom initial props in the dictionary below.
  // They will be passed down to the ViewController used by React Native.
  self.initialProps = @{};
-  return [super application:application didFinishLaunchingWithOptions:launchOptions];
+ BOOL result = [super application:application didFinishLaunchingWithOptions:launchOptions];
+  self.window.rootViewController.view.backgroundColor = [UIColor colorWithRed:1.00 green:0.00 blue:0.40 alpha:1.00];
+  return result;
}

And it will do the trick

Potential workaround:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  self.moduleName = @"MainApp";
  // You can add your custom initial props in the dictionary below.
  // They will be passed down to the ViewController used by React Native.
  self.initialProps = @{};

  BOOL success = [super application:application didFinishLaunchingWithOptions:launchOptions];
  if (success) {
    // Modify as needed to match the main color of your splash.
    self.window.rootViewController.view.backgroundColor = [UIColor colorNamed:@"primary"];
  }
  return success;
}

Here is another example that worked for me:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  self.moduleName = @"RnDiffApp";

  [super application:application didFinishLaunchingWithOptions:launchOptions];
  
  // Hide white flash
  self.window.rootViewController.view.backgroundColor = [[UIColor alloc] initWithRed:0.13 green:0.13 blue:0.13 alpha:1];

  return YES;
}

And here is a link to convert HEX and RGB into UIColor: https://www.uicolor.io/

@zallanx @gabriellend @mysport12 @jafar-jabr @alpha0010: please, refer to my comment above as the proper way to customise your Views and ViewControllers is to override template methods the RCTAppDelegate.h class provides you.

Let me know if they work (as they should). Meanwhile, I’ll close this issue.

I tested the repro and with the proper patch, it is fixed (no white flash anymore). This is the right fix for both architectures:

// At the beginning of the AppDelegate.mm file

#import <React/RCTRootView.h>
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTFabricSurfaceHostingProxyRootView.h>
#endif



// ...

- (UIView *)createRootViewWithBridge:(RCTBridge *)bridge
                          moduleName:(NSString *)moduleName
                           initProps:(NSDictionary *)initProps
{
  UIView * view = [super createRootViewWithBridge:bridge moduleName:moduleName initProps:initProps];
  
#if RCT_NEW_ARCH_ENABLED
  RCTFabricSurfaceHostingProxyRootView * rootView = (RCTFabricSurfaceHostingProxyRootView *)view;
#else
  RCTRootView * rootView = (RCTRootView *)view;
#endif
  
  // workaround:
  UIStoryboard *sb = [UIStoryboard storyboardWithName:@"LaunchScreen" bundle:nil];
  UIViewController *vc = [sb instantiateInitialViewController];
  rootView.loadingView = vc.view;

  return rootView;
}

The createRootViewWithBridge method returns a different UIView * based on the architecture you are building against.

There is still a bug in the New Architecture that makes the flash appear, currently, but it is in the internals. It has to work this way.

We are also working on the APIs to make sure we can soon remove those ugly #ifdefs.

But on the old architecture, it works properly.

Actually, you can do whatever you want with the RootView.

The RCTAppDelegate.h exposes methods that you can override in order to customise your views.

For example, this method allows you to get the default UIView * and to customise it:

//in your AppDelegate.mm

- (UIView *)createRootViewWithBridge:(RCTBridge *)bridge
                          moduleName:(NSString *)moduleName
                           initProps:(NSDictionary *)initProps
{
  UIView * rootView = [super createRootViewWithBridge:bridge moduleName:moduleName initProps:initProps];

  // set your background color

  Return rootView;
}

And there are a method to customise the view controller if needed.

I’m sorry to see this issue only now.

@mysport12 Running into the exact same issue while updating from React Native 0.70.0 to 0.71.0, thanks for posting. I used your example to do something a little different and thought I’d post too in case it’s helpful to anyone. For reference, I am also using Codepush and react-native-bootsplash.

This is what my launch options looked like at React Native 0.70.0, with the main things to note enclosed by **:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  RCTAppSetupPrepareApp(application);

  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];

  #if RCT_NEW_ARCH_ENABLED
  _contextContainer = std::make_shared<facebook::react::ContextContainer const>();
  _reactNativeConfig = std::make_shared<facebook::react::EmptyReactNativeConfig const>();
  _contextContainer->insert("ReactNativeConfig", _reactNativeConfig);
  _bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer];
  bridge.surfacePresenter = _bridgeAdapter.surfacePresenter;
  #endif

  NSDictionary *initProps = [self prepareInitialProps];
  **UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"RnDiffApp", initProps);**

  **if (@available(iOS 13.0, *)) {
    rootView.backgroundColor = [UIColor systemBackgroundColor];
  } else {
    rootView.backgroundColor = [UIColor whiteColor];
  }**

  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [UIViewController new];
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];
  return YES;
}

This is what it looked like after 0.71.0:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  self.moduleName = @"RnDiffApp";
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

Big difference! rootView was completely gone so I couldn’t set backgroundColor that way anymore. Here is what worked for me after tailoring your solution:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  self.moduleName = @"RnDiffApp";
  BOOL success = [super application:application didFinishLaunchingWithOptions:launchOptions];

  // Hide the Codepush white flash
  if (success) {
    RCTRootView *rootView = (RCTRootView *)self.window.rootViewController.view;
   // I wanted to make the backGround color consistent so that's why it's different here. You could also just cut
   // and paste the 'if (@availble(IOS 13.0`, *)) {...' block here.
    rootView.backgroundColor = [[UIColor alloc] initWithRed:0.13 green:0.13 blue:0.13 alpha:1];
  }
  return success;
}

Native files are not my strong suit at all, basically just learning them off the cuff. If anyone comes across this and believes I’m missing something/implemented this incorrectly, I’m very open to correction.

@alpha0010 your solution didn’t get me all the way there but provided me with enough insight to get things working as intended (see below)

BOOL success = [super application:application didFinishLaunchingWithOptions:launchOptions];
if (success) {
  RCTRootView *rootView = (RCTRootView *)self.window.rootViewController.view;
  UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"LaunchScreen" bundle:nil];
  [rootView setLoadingView:[[storyboard instantiateInitialViewController] view]];
  rootView.loadingViewFadeDelay = 0.5;
  rootView.loadingViewFadeDuration = 0.5;
}
return success;

@jafar-jabr I couldn’t seem to get your solution to work as is but like alpha’s proposed solution, gave me clues into what I needed to do.

@cipolleschi Yep, that’s done it 🙂

Thanks for the quick response

Just to make sure, you changed the private _loadingXXX props or did you act on these?

Both tried 😄

Would you be able to prepare a simple reproducer using this template?

Definitely, though might take some time, thank you

Ok, I think that you can do it similarly to what I suggested above:

// In AppDelegate
 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  self.moduleName = @"InteropApp";
  // You can add your custom initial props in the dictionary below.
  // They will be passed down to the ViewController used by React Native.
  self.initialProps = @{};
-  return [super application:application didFinishLaunchingWithOptions:launchOptions];
+ BOOL result = [super application:application didFinishLaunchingWithOptions:launchOptions];
+ UIStoryboard *sb = [UIStoryboard storyboardWithName:@"LaunchScreen" bundle:nil];
+ UIViewController *vc = [sb instantiateInitialViewController];
+ self.window.rootViewController.view.loadingView = vc.view;
+ return result;
}

Property ‘loadingView’ not found on object of type ‘UIView *’ Screenshot 2023-03-20 at 10 19 04 PM

Oh… I understood what’s the culprit.

What we did, to simplify the client code, was to encapsulate the initialization logic into the RCTAppDelegate class. The reason is that , in that way, we can minimize changes for you in the future, with the hope not to touch the AppDelegate ever again (I guess you’d love a simplified update experience, right?).

In doing so, we moved the code as it was to that class, adding some hooks you can override to further customization.

Among these hooks, there is createRootViewWithBridge that is used to create the view at line 55. The problem is that, 2 lines below, we preset the background color for you.

So, given that you are actually updating the only property the AppDelegate is modifying, your change get lost. 😦

So sorry for this. I’ll create a task for myself to expose a last resort method to customize the rootViewController, hence giving you the final word on it and on its rootViewController.view objects.

How does this sound?

I’m sorry for the disruption it may has caused.

The gist of the above being that it seems to work when the loading view is set AFTER the window is set. Combined approaches with code snippets below for conciseness

Pre 0.71.0 (working):

self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
   UIViewController *rootViewController = [UIViewController new];
   rootViewController.view = rootView;
   self.window.rootViewController = rootViewController;
   [self.window makeKeyAndVisible];

   UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"LaunchScreen" bundle:nil];
   UIView *loadingView = [[storyboard instantiateInitialViewController] view];
   [rootView setLoadingView:loadingView];
   rootView.loadingViewFadeDelay = 0.5;
   rootView.loadingViewFadeDuration = 0.5;
   return YES;

0.71.X BOOL success = [super application:application didFinishLaunchingWithOptions:launchOptions]; (working):

BOOL success = [super application:application didFinishLaunchingWithOptions:launchOptions];
  if (success) {
    RCTRootView *rootView = (RCTRootView *)self.window.rootViewController.view;
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"LaunchScreen" bundle:nil];
    [rootView setLoadingView:[[storyboard instantiateInitialViewController] view]];
    rootView.loadingViewFadeDelay = 0.5;
    rootView.loadingViewFadeDuration = 0.5;
  }
  return success;

0.71.X createRootViewWithBridge (not working - launch screen image scaling oversized):

- (UIView *)createRootViewWithBridge:(RCTBridge *)bridge
                          moduleName:(NSString *)moduleName
                           initProps:(NSDictionary *)initProps
{
  RCTRootView * rootView = (RCTRootView *)[super createRootViewWithBridge:bridge moduleName:moduleName initProps:initProps];
  UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"LaunchScreen" bundle:nil];
  [rootView setLoadingView:[[storyboard instantiateInitialViewController] view]];
  rootView.loadingViewFadeDelay = 0.5;
  rootView.loadingViewFadeDuration = 0.5;

  return rootView;

Appreciate the dialog and the assistance on this FWIW. It seems to have helped others as well which is always beneficial.

Thanks @gabriellend, the suggested code worked out great.