Wix react-native-navigation and iOS Launch Screen in 2023

Jimmy Albert
6 min readAug 4, 2023

It is not commonly known that react-native-navigation has its own splashscreen feature, you cannot actually see it in the docs.

“pqkluan” made a post on it in 2018: https://medium.com/@pqkluan/how-to-implement-splash-screen-in-react-native-navigation-ee2184a1a96

Here is the full code in node_modules/react-native_navigation/lib/ios/RNNSplashscreen.m related to this:


#import "RNNSplashScreen.h"
#import <UIKit/UIKit.h>

@implementation RNNSplashScreen

+ (void)showOnWindow:(UIWindow *)window {
CGRect screenBounds = [UIScreen mainScreen].bounds;
CGFloat screenScale = [UIScreen mainScreen].scale;
UIViewController *viewController = nil;

NSString *launchStoryBoard =
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"UILaunchStoryboardName"];
if (launchStoryBoard != nil) { // load the splash from the storyboard that's defined in the
// info.plist as the LaunchScreen
@try {
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:launchStoryBoard
bundle:nil];
UIViewController *launchVC = [storyboard instantiateInitialViewController];

viewController = [[RNNSplashScreen alloc] init];
[viewController addChildViewController:launchVC];
[viewController.view addSubview:launchVC.view];
[launchVC didMoveToParentViewController:viewController];
} @catch (NSException *e) {
UIView *splashView = [[NSBundle mainBundle] loadNibNamed:launchStoryBoard
owner:self
options:nil][0];
if (splashView != nil) {
splashView.frame =
CGRectMake(0, 0, screenBounds.size.width, screenBounds.size.height);
viewController = [[RNNSplashScreen alloc] init];
viewController.view = splashView;
}
}
} else { // load the splash from the DEfault image or from LaunchImage in
// the xcassets
CGFloat screenHeight = screenBounds.size.height;

NSString *imageName = @"Default";
if (screenHeight == 568)
imageName = [imageName stringByAppendingString:@"-568h"];
else if (screenHeight == 667)
imageName = [imageName stringByAppendingString:@"-667h"];
else if (screenHeight == 736)
imageName = [imageName stringByAppendingString:@"-736h"];
else if (screenHeight == 812)
imageName = [imageName stringByAppendingString:@"-812h"];

// xcassets LaunchImage files
UIImage *image = [UIImage imageNamed:imageName];
if (image == nil) {
imageName = @"LaunchImage";

if (screenHeight == 480)
imageName = [imageName stringByAppendingString:@"-700"];
if (screenHeight == 568)
imageName = [imageName stringByAppendingString:@"-700-568h"];
else if (screenHeight == 667)
imageName = [imageName stringByAppendingString:@"-800-667h"];
else if (screenHeight == 736)
imageName = [imageName stringByAppendingString:@"-800-Portrait-736h"];
else if (screenHeight == 812)
imageName = [imageName stringByAppendingString:@"-1100-Portrait-2436h"];
else if (screenHeight == 375)
imageName = [imageName stringByAppendingString:@"-1100-Landscape-2436h"];
else if (screenHeight == 828)
imageName = [imageName stringByAppendingString:@"-1200-Portrait-1792h"];
else if (screenHeight == 896)
imageName =
[imageName stringByAppendingString:screenScale == 2. ? @"-1200-Portrait-1792h"
: @"-1200-Portrait-2688h"];

image = [UIImage imageNamed:imageName];
}

if (image != nil) {
viewController = [[RNNSplashScreen alloc] init];
viewController.view = [[UIImageView alloc] initWithImage:image];
}
}

if (viewController != nil) {
id<UIApplicationDelegate> appDelegate = [UIApplication sharedApplication].delegate;
appDelegate.window.rootViewController = viewController;
[appDelegate.window makeKeyAndVisible];
}
}

- (UIStatusBarStyle)preferredStatusBarStyle {
NSString *styleString = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIStatusBarStyle"];

if ([styleString isEqualToString:@"UIStatusBarStyleLightContent"]) {
return UIStatusBarStyleLightContent;
} else if (@available(iOS 13.0, *)) {
if ([styleString isEqualToString:@"UIStatusBarStyleDarkContent"]) {
return UIStatusBarStyleDarkContent;
}
}
return UIStatusBarStyleDefault;
}

- (BOOL)prefersStatusBarHidden {
return [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIStatusBarHidden"] boolValue];
}

@end

The code tries to:

  1. If a storyboard is available, show it. If it fails to show it, it will check for a NIB file (I am not sure people have a storyboard AND make a nib file at the same time for backup…)
  2. If no Storyboard, it defaults to show the LaunchImage asset

In 2020, Apple has announced the end of LaunchImage asset for all apps in AppStore:

Note: Don’t use a static image for your launch screen. Static images have been deprecated and all App Store apps must use an Xcode storyboard to provide an app’s launch screen by June 30, 2020. Learn more here.”

So… storyboard. For react-native developers, Xcode is not familiar and can be challenging.

Below is the way to configure Xcode to make RNN show the Launch Screen storyboard:

1 — In the previous code, you will need to modify this line :

UIStoryboard *storyboard = [UIStoryboard storyboardWithName:launchStoryBoard
bundle:nil];

To this:

// Remove .storyboard extension
launchStoryBoard = [launchStoryBoard stringByDeletingPathExtension];
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:launchStoryBoard
bundle:nil];

Otherwise, you will get an error from Xcode saying that it cannot find the storyboard LaunchScreen.storyboard. (the extension is not needed in the naming). (I have a current PR)

Note: you might need to use a tool like patch-package to keep your changes. Otherwise the bug will reappear after each pod install.

2 — Remove LaunchImage asset in “Images”

3 — Remove any NIB (& XIB) files related to Launch Screen

4 — I would suggest to also remove any previous Launch Screen storyboard to start from scratch

5 — Add the Launch Screen storyboard “New File > User interface — Launch Screen”

6 — Rename it: “Launch Screen” → “LaunchScreen” (I am not a big fan of spaces in naming files…)

7 — Make sure the ressources are attached during build phase

8 — Select “LaunchScreen.storyboard” in Launch Screen File

9 — In info.plist add the key “UILaunchStoryboardName” if it does not exist already and insert value “LaunchScreen.storyboard”

You are basically done. It will launch with the default Launch Screen storyboard.

Caveat: a quick white screen will appear between the Launch Screen and your first screen. In order to fix it, install react-native-splash-screen and in your first screen add this code inside componentDidMount():

import SplashScreen from 'react-native-splash-screen';

...
componentDidMount() {
...
SplashScreen.hide();
}

Bonus: it will create a nice fade transition.

Optional : Replace the Label with an image

From this:

to this:

1 — Add your image as an asset in Images with all the size (1x, 2x and 3x) and give it a name (like “logo”)

Warning: If you add a custom image (not a system image/icon) through xcassets (images) to the LaunchScreen.storyboard and if your image is quite big, the app will launch with a black screen for 2–3 seconds then it will launch the LaunchScreen.storyboard. The reason you have this black screen is because of Xcode: it bugs when you have images that are big (looks like more than 1mb), I suggest you make images @1x @2x and @3x at a few kb each. You can find more info on this SO post.

2 — In LaunchScreen.storyboard, remove the 2 labels view

3 — Then add an Image view by 1) Clicking the + button on the top right of the screen (library) 2) Searching for “Image” 3) Then Dragging the view under the “Safe Area” view

4 — The image view is empty. We must set it as our “logo”. On the right panel, select “Show the attributes inspector” icon and find “Logo” in the Image field

5 — Now, if we want to make sure our logo is well centered on all type of screens we must modify the default constraints: select the ruler icon then in “Autoresizing” unselect the left and top constraints and select the two middle ones. You will also need to move slightly your image to the center of the screen by aligning it to the horizontal and vertical centered lines

Voilà !

--

--