Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
mysport12 opened this issue Jan 23, 2023 · 46 comments
Labels
Needs: Triage 🔍 Platform: iOS iOS applications. Type: Upgrade Issue Issues reported from upgrade issue form

Comments

@mysport12
Copy link
Contributor

mysport12 commented Jan 23, 2023

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).

@mysport12 mysport12 added Needs: Triage 🔍 Type: Upgrade Issue Issues reported from upgrade issue form labels Jan 23, 2023
@react-native-bot react-native-bot added the Platform: iOS iOS applications. label Jan 23, 2023
@alpha0010
Copy link
Contributor

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;
}

@jafar-jabr
Copy link

jafar-jabr commented Jan 24, 2023

@mysport12
this worked for me:

  UIStoryboard *sb = [UIStoryboard storyboardWithName:@"LaunchScreen" bundle:nil];
  UIViewController *vc = [sb instantiateInitialViewController];
  
  ((RCTRootView *)self.window).loadingView = vc.view;

@mysport12
Copy link
Contributor Author

Thank you both for some potential solutions! I will evaluate and comment back.

@mysport12
Copy link
Contributor Author

@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.

@gabriellend
Copy link

gabriellend commented Feb 7, 2023

@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.

@cipolleschi
Copy link
Contributor

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.

@zallanx
Copy link

zallanx commented Feb 7, 2023

Thanks @gabriellend, the suggested code worked out great.

@cipolleschi
Copy link
Contributor

@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.

@gabriellend
Copy link

gabriellend commented Feb 9, 2023

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/

@gabriellend
Copy link

@cipolleschi Where would that snippet go in AppDelegate? In launch options?

@cipolleschi
Copy link
Contributor

@gabriellend That's another method of the app delegate.

So you can just add the snippet below the application:didFinishLaunchingWithOption:

So, something like:

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

  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

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

  rootView.backgroundColor = [[UIColor alloc] initWithRed:0.13 green:0.13 blue:0.13 alpha:1];

  return rootView;
}

This should simplify the app code quite a bit, limiting the possibility that something can go wrong! 😉

@mysport12
Copy link
Contributor Author

@cipolleschi I was able to get that approach to work for preventing the flash, but the storyboard launch screen size was all out of whack so for now will stick with the original approach I posted above (in 'didFinishLaunchingWithOptions) for my use case. Agree though that for setting a background color it works perfectly fine

@cipolleschi
Copy link
Contributor

Uhm.. @mysport12 that should not be the case...

Just to make sure, your code of the createRootView was something like this?

- (UIView *)createRootViewWithBridge:(RCTBridge *)bridge
                          moduleName:(NSString *)moduleName
                           initProps:(NSDictionary *)initProps
{
  UIView * rootView = [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;

?

React Native does nothing specific with the View Controller, so as long as you use the UIView created by the framework, you should be good...

Also, why you need to access the storyboard to retrieve the loading view? Can't it be init in isolation?

(I'm trying to understand the use case, to make sure we can support everyone of you! 😊)

@mysport12
Copy link
Contributor Author

Yes that's correct. Only difference being the rootView declared as RCTRootView to have access to the loading view methods/params.

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

We want the loading view to be the launch screen which just has a background color and a centered image (some of my apps have a slightly more complicated image arrangement). That way launch -> loading -> JS splash are consistent so the user doesn't perceive a difference as those transitions take place.

The behavior witnessed was that the centered image was scaled pretty large to the point where it was clipped by the edges of the device. It almost seems like the bounds weren't correct. Could it be the order of operations of when the following line gets called?

self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];

in the code block from the AppDelegate.mm from react native

UIView *rootView = [self createRootViewWithBridge:self.bridge moduleName:self.moduleName initProps: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 = [self createRootViewController];
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];

Prior to 0.71.0 when our individual apps called the code, the self.window logic was executed prior to the loading view logic. Just spitballing. I will try and dig into it more tonight/tomorrow.

@mysport12
Copy link
Contributor Author

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.

@gabriellend
Copy link

gabriellend commented Feb 10, 2023

@cipolleschi Ok I tried your example but it didn't take care of the white flash so I'm just going to stick with my original fix. The white flash happens when CodePush finishes downloading the new JS bundle and the JS app is restarted. I don't know enough to add any hypotheses of why your solution didn't work in my case, but I really appreciate your help and this discussion!

@cipolleschi
Copy link
Contributor

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.

@gabriellend
Copy link

@cipolleschi I'm not familiar enough with the native side of things to totally follow so this could be wrong but the line of code you reference at your link for "we preset the background color" is no longer present in React Native 0.71. It sounds like you are saying that since it's being set there, any changes we make later (i.e. with your new code) are being overridden. But in 0.71, that "setting" block is gone so it seems like it's not being set and therefore not overriding anything. I don't know if that's clear. If you want to find another solution that is better, that would be great, but honestly I'm happy with the solution I found for now. It's not been a huge disruption at all, and I thank you for your attention to this issue.

cipolleschi added a commit to cipolleschi/react-native that referenced this issue Feb 20, 2023
Summary:
When we introduced the RCTAppDelegate library, we prepared some template methods for the user to customise their views.

However, after they customized their view, we were chaing the background color to match the system background. This would actually override the background color they set in their own customisation step.

This change make sure that we set the background color before they apply their customisations. In this way, we set the background color and, if they want, they can change it and that changw would be honoured.

This change also fixes [this issue](facebook#35937)

Changelog
[iOS][Fixed] - Honour background color customisation in RCTAppDelegate

Differential Revision: D43435946

fbshipit-source-id: 0fe740eba816f42cbbdaa8ec65e6f1e6e76c7dc8
cipolleschi added a commit to cipolleschi/react-native that referenced this issue Feb 20, 2023
Summary:
Pull Request resolved: facebook#36215

When we introduced the RCTAppDelegate library, we prepared some template methods for the user to customise their views.

However, after they customized their view, we were chaing the background color to match the system background. This would actually override the background color they set in their own customisation step.

This change make sure that we set the background color before they apply their customisations. In this way, we set the background color and, if they want, they can change it and that changw would be honoured.

This change also fixes [this issue](facebook#35937)

## Changelog
[iOS][Fixed] - Honour background color customisation in RCTAppDelegate

Differential Revision: D43435946

fbshipit-source-id: 0e02c1b3bbbbfb1c7d75fbd08a74140da8e6cf5c
facebook-github-bot pushed a commit that referenced this issue Feb 20, 2023
Summary:
Pull Request resolved: #36215

When we introduced the RCTAppDelegate library, we prepared some template methods for the user to customise their views.

However, after they customized their view, we were chaing the background color to match the system background. This would actually override the background color they set in their own customisation step.

This change make sure that we set the background color before they apply their customisations. In this way, we set the background color and, if they want, they can change it and that changw would be honoured.

This change also fixes [this issue](#35937)

## Changelog
[iOS][Fixed] - Honour background color customisation in RCTAppDelegate

Reviewed By: cortinico

Differential Revision: D43435946

fbshipit-source-id: cdbdbd5b07082ae7843a4dab352dd1195c69e036
@gtokman
Copy link

gtokman commented Mar 15, 2023

I'm seeing a blank white screen before my splash screen after upgrading too.

Previously I solved the problem by adding this snippet below into didFinshLaunching:

self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [UIViewController new];
  rootViewController.view.backgroundColor = [UIColor colorWithRed:1.00 green:0.00 blue:0.40 alpha:1.00];
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];

I tried using what you suggested @cipolleschi but I didn't have any luck.

@rifad4u
Copy link

rifad4u commented Mar 19, 2023

@cipolleschi How i can show the splash screen in the transition ,instead of showing a background color as u did above?

@cipolleschi
Copy link
Contributor

@rifad4u it really depends on the kind of splashscreen you have.

Is it a static image, handled by the catalog/plist? Is it a custom views?

It may help to see what was your code before 0.71, so I can understand it better and suggest the best path forward.

@rifad4u
Copy link

rifad4u commented Mar 20, 2023

@rifad4u it really depends on the kind of splashscreen you have.

Is it a static image, handled by the catalog/plist? Is it a custom views?

It may help to see what was your code before 0.71, so I can understand it better and suggest the best path forward.

It is a static image only and i have configured it as like below
Screenshot 2023-03-20 at 4 13 30 PM

@cipolleschi
Copy link
Contributor

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;
}

@rifad4u
Copy link

rifad4u commented Mar 20, 2023

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

@cipolleschi
Copy link
Contributor

Right, because the View returned by the rootViewController is a UIView *, but you can safely downcast it to RCTRootView.

So, the code I posted above must be changed to:

//....
RCTRootView * rootView = (RCTRootView *) self.window.rootViewController.view;
rootView.loadingView = vc.view;
Return result;

@Dajust
Copy link

Dajust commented Mar 21, 2023

+ 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;

After it's fixed in a future release, should we still do it this way?

@cipolleschi
Copy link
Contributor

@Dajust No, after the fix lands, the right way to do it will be to override the createRootViewWithBridge in your AppDelegate:

- (UIView *)createRootViewWithBridge:(RCTBridge *)bridge
                          moduleName:(NSString *)moduleName
                           initProps:(NSDictionary *)initProps
{
  // get the base, preconfigured view for React native
  UIView * view = [super createRootViewWithBridge:bridge
                          moduleName:moduleName
                           initProps:initProps];
  // Add your customisations
  view.backgroundColor = [UIColor colorWithRed:1.00 green:0.00 blue:0.40 alpha:1.00];

  // Return the customised view
  return view;
}

@nazmeln
Copy link

nazmeln commented Apr 7, 2023

Hey all,
In case someone struggles as I did with showing a splash screen instead of the custom background, here's the code snippet which works for me with react-native-bootsplash library.
❗️Note: order matters 😅

  self.initialProps = @{};

  BOOL result = [super application:application didFinishLaunchingWithOptions:launchOptions];

  [RNBootSplash initWithStoryboard:@"BootSplash" rootView:self.window.rootViewController.view];

  // Hide white flash
  self.window.rootViewController.view.backgroundColor = [UIColor colorWithRed: 0.03 green: 0.09 blue: 0.24 alpha: 1.00];;

  return result;

OlimpiaZurek pushed a commit to OlimpiaZurek/react-native that referenced this issue May 22, 2023
Summary:
Pull Request resolved: facebook#36215

When we introduced the RCTAppDelegate library, we prepared some template methods for the user to customise their views.

However, after they customized their view, we were chaing the background color to match the system background. This would actually override the background color they set in their own customisation step.

This change make sure that we set the background color before they apply their customisations. In this way, we set the background color and, if they want, they can change it and that changw would be honoured.

This change also fixes [this issue](facebook#35937)

## Changelog
[iOS][Fixed] - Honour background color customisation in RCTAppDelegate

Reviewed By: cortinico

Differential Revision: D43435946

fbshipit-source-id: cdbdbd5b07082ae7843a4dab352dd1195c69e036
@professorkolik
Copy link

@cipolleschi We would appreciate if documentation can be updated => https://reactnative.dev/docs/publishing-to-app-store#pro-tips

As well as how we can disable animation. With previous implementation all was good. Now we have to patch-package changes in RN0.71 and RN0.72 to put the old solution between
"[self.window makeKeyAndVisible]" and before "return YES;" in RCTAppDelegate

@cipolleschi
Copy link
Contributor

cipolleschi commented Jun 23, 2023

Hi @professorkolik, thank for the question. Let me understand, it's only a matter of updating the doc or you feel blocked somehow?

The proper way to apply these changes:

UIStoryboard *sb = [UIStoryboard storyboardWithName:@"LaunchScreen" bundle:nil];
  UIViewController *vc = [sb instantiateInitialViewController];
  rootView.loadingView = vc.view;

In 0.71 and onward, is to override

- (UIView *)createRootViewWithBridge:(RCTBridge *)bridge
                          moduleName:(NSString *)moduleName
                           initProps:(NSDictionary *)initProps;

in your AppDelegate.mm with something like:

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

  return rootView;
}

We can update the documentation with this approach, if that's what you were looking for (or we welcome contributions to the website too! 😄 It is backed by this GitHub repo: https://github.com/facebook/react-native-website ).

@professorkolik
Copy link

@cipolleschi Thank you for response!

It's both documentation and block. And one comes from other, so we are blocked and I think may be I do smth wrong.

I tried all the suggestions. It actually works, but not as expected or not as before.

To give a background:

We had a UIStoryBoard with app icon, when the bundle loaded we have the same SplashScreen component but with loading indicator, when StoryBoard hides for our users there is no change in behaviour just loading spinner appears.

When I implement new way of controlling the Flash screen. I got behaviour that UIStoryBoard hides but with animation.
I checked in sources of RCTRootView that animation is applied with 2 options

_loadingViewFadeDelay = 0.25;
_loadingViewFadeDuration = 0.25;

Changing this values to 0, doesn't work as well.

To summarize, if I just put the solution from docs into didFinishLaunchingWithOptions of RCTAppDelegate.mm, between

[self.window makeKeyAndVisible] and before return YES; I'm getting desired behaviour

Thank you again, any hint is appreciated 🙏

@cipolleschi
Copy link
Contributor

Ah... that's weird. 🤔
The two behaviors should be identical.

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

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

@professorkolik
Copy link

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

@joegoodall1
Copy link

joegoodall1 commented Jun 30, 2023

Hi @cipolleschi

Just trying to work out, with version 0.71.xx, the correct way to go about preventing the blank screen flash between the splash screen and the display of the root application view.

These are the now outdated but still live docs. Have tried the solutions on this thread but no luck so far.

@cipolleschi
Copy link
Contributor

@joegoodall1 thanks for trying. Yeah, that doc is live but outdated.
If you could create a repro using the template above, I can try to figure out how to fix it properly and then I'll update the doc accordingly.

@joegoodall1
Copy link

joegoodall1 commented Jul 2, 2023

@cipolleschi

Here you go https://github.com/joegoodall1/whiteflashsplash

Intentionally extremely simple changes (changed background of splash screen and 1st RN screen to red so white flash is visible)

splash.mov

@professorkolik
Copy link

@joegoodall1 before @cipolleschi will take a look, I don't see you applied suggested changes in AppDelegate.mm file.

So I'm not sure what you expect

@joegoodall1
Copy link

@professorkolik Thanks for the quick response

Do you mean like this?

#import "AppDelegate.h"

#import <React/RCTBundleURLProvider.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  self.moduleName = @"ReproducerApp";
  // 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];
}

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

  return rootView;
}

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
  return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}

@end

Seeing this error with that code

Untitled 2

@professorkolik
Copy link

@joegoodall1 let is a typo you don't need it, remove it
maybe you'll need to type rootViiew
UIView *rootView = .....

@cipolleschi
Copy link
Contributor

Yeah, the let is a typo I made from my Swift background. 🤦
@professorkolik is right, in suggesting to use UIView * in place of let. I updated the suggestion above.

@joegoodall1
Copy link

@cipolleschi @professorkolik

Now seeing this error

Untitled

@cipolleschi
Copy link
Contributor

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.

@joegoodall1
Copy link

@cipolleschi Yep, that's done it 🙂

Thanks for the quick response

@fadi-quader-mox
Copy link
Contributor

fadi-quader-mox commented Aug 18, 2023

This is the way i did it to to change rootView background

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  self.moduleName = @"DApp";
  // 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 = @{};

  // check if app is loaded
  BOOL success = [super application:application didFinishLaunchingWithOptions:launchOptions];
  if (success) {
    UIViewController *rootViewController = [UIApplication sharedApplication].delegate.window.rootViewController;
  
    // Force disable dark mode
    if (@available(iOS 13.0, *)) {
      rootViewController.view.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
    }
    
    // change rootView background
    rootViewController.view.backgroundColor = [[UIColor alloc] initWithRed:0.0f green:0.0f blue:0.0f alpha:1];
  }
  
  return success;
}

@cipolleschi
Copy link
Contributor

@fadi-quader-mox this is not the right way to do it.
You should override this method to customize the appearance of your view.

@facebook facebook locked and limited conversation to collaborators Aug 18, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Needs: Triage 🔍 Platform: iOS iOS applications. Type: Upgrade Issue Issues reported from upgrade issue form
Projects
None yet
Development

Successfully merging a pull request may close this issue.