facebook / react-native

A framework for building native applications using React
https://reactnative.dev
MIT License
118.41k stars 24.25k forks source link

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

Closed mysport12 closed 1 year ago

mysport12 commented 1 year ago

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

alpha0010 commented 1 year ago

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 commented 1 year ago

@mysport12 this worked for me:

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

  ((RCTRootView *)self.window).loadingView = vc.view;
mysport12 commented 1 year ago

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

mysport12 commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

Thanks @gabriellend, the suggested code worked out great.

cipolleschi commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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

cipolleschi commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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

gtokman commented 1 year ago

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.

cipolleschi commented 1 year ago

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

rifad4u commented 1 year ago

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

cipolleschi commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago
+ 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 commented 1 year ago

@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 commented 1 year ago

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;
professorkolik commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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

https://github.com/facebook/react-native/assets/8888799/5aeb29ce-f141-4aac-a335-688a4364bea7

professorkolik commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

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

cipolleschi commented 1 year ago

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 commented 1 year ago

@cipolleschi @professorkolik

Now seeing this error

Untitled

cipolleschi commented 1 year ago

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 commented 1 year ago

@cipolleschi Yep, that's done it πŸ™‚

Thanks for the quick response

fadi-quader-mox commented 1 year ago

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 commented 1 year ago

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