facebook / react-native

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

React Native App with implemented Carplay doesnt show content on carplay screen if IPhone App is terminated #41777

Closed zerobertelprivat closed 11 months ago

zerobertelprivat commented 11 months ago

Description

I have build a well working React Native App (thanks to the RN Team and supporters) with the ability to play podcasts via App and Carplay. To implement Carplay i have splitted the App in 2 Scenes via the Info.plist. Additional adjustments in the AppDelegate.m were also neccessary, i have copied the whole RCT AppDelegate to ensure the correct sequence of code in the adjusted function:

` @implementation AppDelegate

(BOOL)application:(UIApplication )application didFinishLaunchingWithOptions:(NSDictionary )launchOptions { / Original Code Start -> code of clean default function / self.moduleName = @"app"; // 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] // ^^ removed because of SceneDelegate logic for carplay / Original Code End /

/ CUSTOM_IMPLEMENTATION_START /

/ IMPORTANT NOTE: whole following code replaces return [super application:application didFinishLaunchingWithOptions:launchOptions]; of original version -> neccessary to split app in scenes for carplay -> after every React Native Update you have to copy code Sections of the RCTAppDelegate "application" function except the uncomment "self.window" etc code /

/ Copied code from RCTAppDelegate application function (start) / BOOL enableTM = NO;

if RCT_NEW_ARCH_ENABLED

enableTM = self.turboModuleEnabled;

endif

RCTAppSetupPrepareApp(application, enableTM);

if (!self.bridge) { self.bridge = [self createBridgeWithDelegate:self launchOptions:launchOptions]; }

if RCT_NEW_ARCH_ENABLED

self.bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:self.bridge contextContainer:_contextContainer]; self.bridge.surfacePresenter = self.bridgeAdapter.surfacePresenter;

[self unstable_registerLegacyComponents];

endif

NSDictionary initProps = [self prepareInitialProps]; UIView rootView = [self createRootViewWithBridge:self.bridge moduleName:self.moduleName initProps:initProps];

// self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; // UIViewController *rootViewController = [self createRootViewController]; // rootViewController.view = rootView; // self.window.rootViewController = rootViewController; // [self.window makeKeyAndVisible]; // ^^ // ^^ Ignored Code from RCTAppDelegate because of SceneDelegate logic for carplay // ^^ Code is moved into SceneDelegate class

/ Copied code from RCTAppDelegate application function (end) /

// make bridge and rootview shareable to enable acces from SceneDelegate [[[RNBridgeInstanceHolderObjC alloc]init] setBridgeAndRootView:(self.bridge) rootView:(rootView)]; // custom_carplay

[RNSplashScreen show]; // custom_splashscreen

/ Copied code from RCTAppDelegate application function (start) / return YES; / Copied code from RCTAppDelegate application function (end) /

/ CUSTOM_IMPLEMENTATION_END / }

/ CUSTOM_IMPLEMENTATION_START / // Copied code from RCTAppDelegate (start)

ifdef RCT_NEW_ARCH_ENABLED

// Hardcoding the Concurrent Root as it it not recommended to // have the concurrentRoot turned off when Fabric is enabled. initProps[kRNConcurrentRoot] = @([self fabricEnabled]);

endif

return initProps; } // Copied code from RCTAppDelegate (end) / CUSTOM_IMPLEMENTATION_END / `

No the Problem is: Music Apps like Spotify, Tidal etc show carplay content if the phone apps are terminated and i click on the App Icon in Carplay Simulator. My App doesnt so, it shows an empty screen. If the phone app is in background or active, everything is working well. I have no idea what i am doing wrong. Please help! Thanks in advance!

Some code of the CarPlay SceneDelegate:

import Foundation import CarPlay class SceneDelegateCarPlay: UIResponder, CPTemplateApplicationSceneDelegate { ... func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didConnect interfaceController: CPInterfaceController) { print("SceneDelegateCarPlay.templateApplicationScene(), didConnect") // start render content ... }

"templateApplicationScene" is called and render content when:

  1. Carplay App is closed & Iphone App is active or in background -> Click on Carplay App Icon
  2. Carplay App is open (no content visible) & Iphone App is terminated -> Start Iphone App

I wonder how i can render content outside of a case like these

React Native Version

0.72.7

Output of npx react-native info

System: OS: macOS 14.1.1 CPU: (10) arm64 Apple M1 Pro Memory: 98.50 MB / 16.00 GB Shell: version: "5.9" path: /bin/zsh Binaries: Node: version: 16.17.1 path: ~/.nvm/versions/node/v16.17.1/bin/node Yarn: version: 1.22.19 path: ~/Projekte/Mediapioneer/app/node_modules/.bin/yarn npm: version: 8.15.0 path: ~/.nvm/versions/node/v16.17.1/bin/npm Watchman: version: 2023.10.23.00 path: /opt/homebrew/bin/watchman Managers: CocoaPods: version: 1.14.3 path: /opt/homebrew/bin/pod SDKs: iOS SDK: Platforms:

Steps to reproduce

See description

Snack, screenshot, or link to a repository

Bildschirmfoto 2023-12-04 um 07 42 04
github-actions[bot] commented 11 months ago
:warning: Missing Reproducible Example
:information_source: We could not detect a reproducible example in your issue report. Please provide either:
  • If your bug is UI related: a Snack
  • If your bug is build/update related: use our Reproducer Template. A reproducer needs to be in a GitHub repository under your username.
cortinico commented 11 months ago

Have you considered using https://github.com/birkir/react-native-carplay or other similar community contributed projects to do Carplay with React Native?

zerobertelprivat commented 11 months ago

Have you considered using https://github.com/birkir/react-native-carplay or other similar community contributed projects to do Carplay with React Native?

I have considered, but i wasnt sure if this lib will fill my needs and it seems to be used rarely. Because the carplay template logic is managed from React Native environment, i cannot imagine how carplay should work without launching the phone app (?)

zerobertelprivat commented 11 months ago

I have found the problem:

[RNSplashScreen show];

blocks the application function, if i comment it out, the carplay screen gives me content without iphone app open ....

cortinico commented 11 months ago

I have found the problem:

Closing. I also advice you to seek support over at https://github.com/birkir/react-native-carplay/issues

bitcrumb commented 7 months ago

@cortinico Why was this issue closed? This is a genuine issue/shortcoming in how React Native is set up in the AppDelegate.

It is not compatible with how CarPlay requires the use of scenes to setup either the phone or car app. See hits excerpt from this PR on the react-native-carplay repository.

A React Native app usually calls the super-method in its AppDelegate, which is implemented in React Native's own RCTAppDelegate. The problem with this is that RCTAppDelegate assumes a phone usage and creates a rootViewController along with a window for the app to be displayed in. This leads to problems when launching the app on the CarPlay-client first, since CarPlay does not require a rootViewController or a window to display its views.

Developers shouldn't have to go through hoops like those in the linked PR when wanting to add CarPlay support to a React Native app.

Ideally the initialisation code in the AppDelegate can be more efficiently tailored/applied when using scenes without having to go copy/paste from internals.

TLDR: This is not an issue that can only be solved by react-native-carplay, ideally developers can add scenes and are able to move initialization logic over to the scenes logic without too much hassle.

cortinico commented 7 months ago

This is a genuine issue/shortcoming in how React Native is set up in the AppDelegate.

I'll pass this over to @cipolleschi to answer when he'll be back (as he's on vacation now). He can provide more insights on what's the best course of action here

bitcrumb commented 7 months ago

Maybe I should also add that this issue (which stems from using scenes) is not unique to CarPlay. If you want your app to support multiple windows, you also have to opt-in to using scenes on iOS (on iPad you can place multiple windows of the same app next to eachother). Each of those windows will need to have its own root view controller.

The issue now is that a root view controller already gets created in the application:didFinishLaunchingWithOptions: method. While when using scenes you are supposed to create the root view controllers in the scene delegates. This root view controller (created in the app delegate) will live next to te ones created in the scenes.

This leads to an unneccessary extra root view controller being present (which doesn't play nice with CarPlay apparently).

I think one way to make the current RN initialization setup compatible with scenes on iOS, would be to abstract the logic that is now contained in application:didFinishLaunchingWithOptions: into separate methods (one for setting up the bridge and one for create the root view & view controller and call those from the app AppDelegate as such:

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    BOOL result = [super application:application didFinishLaunchingWithOptions:launchOptions]; // this doesn't contain any actual setup anymore

    if (result) {
        [self initBridge];

        self.window = [self createWindow];
        self.window.makeKeyAndVisible()
    }

    return result;
}

@end

The initBridge method would do all initialization (mainly setting up the bridge if needed), and the createWindow method would contain teh code for creating the root view controller (with root view) and creating a window to which to asign these.

This would allow apps that need to use scenes to remove app delegate implementation and only call initBridge in for instance the CarPlay scene (via UIApplication.shared.delegate).

In a PhoneScene you could opt to invoke both initBridge and createWindow and assign the result of createWindow to the window property of the scene.

Any feedback welcome ofcourse.

I'm going to go ahead and tag @danielkuhn in here as well, maybe he has some feedback (or corrections) on this too, since this is how I understand the problem space at this moment.

Note: I know this kind of re-introduces some "complexity" into the AppDelegate vs. merely extending RCTAppDelegate.mm, and this probably will require tools like Expo to adapt as well (which now wrap the AppDelegate logic).

Another option to work around this is to introduce some extra decision logic in RCTAppDelegate based on a new instance variable set in the app delegate. This way you could keep on extending RCTAppDelegate, e.g. introducing a preventWindowCreation boolean:

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

  self.preventWindowCreation = YES; // should be NO by default

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

This preventWindowCreation could be than taken into account to conditionally create the root view controller. Ideally this creation is still abstracted away into its own method so it can still be called directly from a (phone) scene.

DanielKuhn commented 7 months ago

Hey @bitcrumb , thanks for keeping me in the loop. Of course it would be awesome if React Native could support Scenes and switch the basic iOS setup to SceneDelegates instead of the single AppDelegate. Scenes have become the standard ever since iOS 13, therefore if you want to write a React Native app that supports multiple windows (like CarPlay, multitasking/split view on iPad or supporting external displays on iOS or iPadOS) you need to implement SceneDelegates anyways.

Here's some literature on the topic: Mutli Window Support is happening (from 2019): "Next time you create a new Xcode project you’ll see your AppDelegate has split in two: AppDelegate.swift and SceneDelegate.swift. This is a result of the new multi-window support that landed with iPadOS [...] scene delegates are there to handle one instance of your app’s user interface. So, if the user has created two windows showing your app, you have two scenes, both backed by the same app delegate."

Single vs. Multiple instances:

https://www.appypie.com/scene-delegate-app-delegate-xcode-11-ios-13 https://manasaprema04.medium.com/scene-delegate-vs-appdelegate-86e22dc17fcb ...

There's a ton of articles about this, no need to link them all.

It would be a great step forward for iOS development in React Native to switch from the old AppDelegate.h and AppDelegate.mm to a modern AppDelegate.swift and SceneDelegate.swift setup - just like on the Java side React Native now switched from Java to Kotlin.

I outlined pretty much everything I know about the topic in my PR over at react-native-carplay and the README in the included example app. I'd love to see this come to future versions of React Native and would love to help of course!

cipolleschi commented 7 months ago

Hi everyone, sorry for the late reply, I was on PTO.

On the matter of moving from the AppDelegate to the SceneDelegate: yes, that would be amazing and the right thing to do. However, right now, we don't have capacity to work on that as we have some more urgent task to tackle. The change is not big per sé, but has the implication that it will "force" a migration from the AppDelegate to the SceneDelegate to the whole ecosystem, and we don't have capacity to support the users in that migration as we are focused on the New Architecture at the moment.

So, for your specific use case at the moment, you should implement the Scene Delegate in your app yourself.

On the Swift topic, migrating to Swift from Objective-C is a theme dear to me and very close to my heart. However, there are technical limitation at the moment that prevent us from doing it, mainly some incompatibilities between C++ and Swift. Yes, this year Apple started adding Swift/C++ interop, but it is not complete yet and there are some feature missing that we need (for example Inheritance: the Fabric Components base class is in C++ and Swift Classes cannot extends C++ classes).

Plus, the C++ structure of React Native is not compatible with Swift (they are not Clang modules, sadly). Changing that will require a huge refactoring of the codebase and changes to almost all the #import/#include directives and, again, we don't have capacity to follow with this effort at the moment.

DanielKuhn commented 7 months ago

Hi @cipolleschi , thank you for your reply.

I understand that this is a big task that needs lots of planning and migration support. But wouldn't it be a fairly easy and straightforward first step to at least change the AppDelegate implementation to Swift, while keeping everything within react native (i.e. RCTAppDelegate) in ObjC? Or at least make it an option for create-react-native-app, react-native init and create-expo-app?

Of course this would require maintainers of packages with native wiring in AppDelegate to also update their documentation to offer example code in both languages (like Apple does on their docs pages) but in times of ChatGPT and the like, I would guess that most developers could also figure out these rather small adjustments/translations themselves - especially if they explicitly opted in to Swift on app creation.

cipolleschi commented 7 months ago

That's actually one of the biggest! 😅 Converting the AppDelegate to Swift is troublesome, for various reasons: