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

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

Closed
zerobertelprivat opened this issue Dec 4, 2023 · 12 comments
Labels
Resolution: Answered When the issue is resolved with a simple answer

Comments

@zerobertelprivat
Copy link

zerobertelprivat commented Dec 4, 2023

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)

  • (NSDictionary *)prepareInitialProps
    {
    NSMutableDictionary *initProps = self.initialProps ? [self.initialProps mutableCopy] : [NSMutableDictionary new];

#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:
- DriverKit 23.0
- iOS 17.0
- macOS 14.0
- tvOS 17.0
- watchOS 10.0
Android SDK: Not Found
IDEs:
Android Studio: 2022.3 AI-223.8836.35.2231.11090377
Xcode:
version: 15.0.1/15A507
path: /usr/bin/xcodebuild
Languages:
Java:
version: 11.0.16.1
path: /usr/bin/javac
Ruby:
version: 3.2.2
path: /opt/homebrew/opt/ruby/bin/ruby
npmPackages:
"@react-native-community/cli": Not Found
react:
installed: 18.2.0
wanted: 18.2.0
react-native:
installed: 0.72.7
wanted: 0.72.7
react-native-macos: Not Found
npmGlobalPackages:
"react-native": Not Found
Android:
hermesEnabled: true
newArchEnabled: false
iOS:
hermesEnabled: true
newArchEnabled: false

Steps to reproduce

See description

Snack, screenshot, or link to a repository

Bildschirmfoto 2023-12-04 um 07 42 04
@github-actions github-actions bot added the Needs: Repro This issue could be improved with a clear list of steps to reproduce the issue. label Dec 4, 2023
Copy link

github-actions bot commented Dec 4, 2023

⚠️ Missing Reproducible Example
ℹ️ 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
Copy link
Contributor

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

@zerobertelprivat
Copy link
Author

zerobertelprivat commented Dec 5, 2023

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 (?)

@github-actions github-actions bot added Needs: Attention Issues where the author has responded to feedback. and removed Needs: Author Feedback labels Dec 5, 2023
@zerobertelprivat
Copy link
Author

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
Copy link
Contributor

I have found the problem:

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

@cortinico cortinico added Resolution: Answered When the issue is resolved with a simple answer and removed Needs: Triage 🔍 Needs: Repro This issue could be improved with a clear list of steps to reproduce the issue. Needs: Attention Issues where the author has responded to feedback. labels Dec 5, 2023
@bitcrumb
Copy link

bitcrumb commented Mar 26, 2024

@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
Copy link
Contributor

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
Copy link

bitcrumb commented Mar 27, 2024

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
Copy link

DanielKuhn commented Mar 27, 2024

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:

  • AppDelegate: Traditionally, the AppDelegate was responsible for managing a single instance of UI. It provided the app’s window object that acted as the root view for the entire app.
  • SceneDelegate: With the introduction of scenes, the SceneDelegate allows for multiple instances of UI within a single app. Each scene has its own window object, and the SceneDelegate is responsible for managing the UI and events specific to that scene.

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
Copy link
Contributor

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
Copy link

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
Copy link
Contributor

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

  • On the Community side, it forces a migration. The react-native-upgrade-helper will see the new Swift files as completely new files and will ask all the user to change their current implementation.
  • On the technical side, there are issues with moving back and forth the architectures. I couldn't find a way to implement the AppDelegate in Swift in a way that you can build with the old architecture and then switch to the new architecture and then switch back again without issue. The main problem here is that our C++ layer for the New Architecture is not properly configured (e.g. there are no modulemaps, among other problems) so Swift has trouble building it.
    As we are focusing on the New Architecture right now, this is a big blocker. Hopefully, when the rollout of the New Arch is done, I'll have more time to look into the Swift blockers and try to solve them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Resolution: Answered When the issue is resolved with a simple answer
Projects
None yet
Development

No branches or pull requests

5 participants