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

Standalone in iOS not loading up JS template code. #199

Open
EliG-TA opened this issue Jul 10, 2024 · 11 comments
Open

Standalone in iOS not loading up JS template code. #199

EliG-TA opened this issue Jul 10, 2024 · 11 comments
Labels
bug Something isn't working

Comments

@EliG-TA
Copy link

EliG-TA commented Jul 10, 2024

Hey! I cannot understand why I am having so much difficulty getting this library setup for IOS. I got it working with minimal effort on my part for Android. This issue is happening after I followed meticulously what the documentations say on setting up for IOS, but to no avail. What could I possibly be doing wrong. FYI, I'm trying very hard to use this library with my Expo project even though this is a bare bones React Native library and it was not built for this use case. However, I feel like I am getting very close. I am fully aware that this library has been transitioning from Objective-C in favor of Swift for much of the code base, however I since I am trying to keep my App stable being that it uses a lot of Expo, I would like to keep it in Objective-C if possible.

Here is the error output:

⚠️ ld: Could not find or use auto-linked framework 'CoreAudioTypes': framework 'CoreAudioTypes' not found
❌ clang: error: linker command failed with exit code 1 (use -v to see invocation)

Here is my relevant project folder layout and code:

.
└── ./ios/
├── ./ios/build/
├── ./ios/Pods/
├── ./ios/MyProject/
│ ├── ./ios/MyProject/Images.xcassets/
│ ├── ./ios/MyProject/Supporting/
│ ├── ./ios/MyProject/AppDelegate.h
│ ├── ./ios/MyProject/AppDelegate.mm
│ ├── ./ios/MyProject/CarSceneDelegate.h
│ ├── ./ios/MyProject/CarSceneDelegate.m
│ ├── ./ios/MyProject/Info.plist
│ ├── ./ios/MyProject/main.m
│ ├── ./ios/MyProject/noop-file.swift
│ ├── ./ios/MyProject/PhoneSceneDelegate.h
│ ├── ./ios/MyProject/PhoneSceneDelegate.m
│ ├── ./ios/MyProject/SplashScreen.storyboard
│ ├── ./ios/MyProject/MyProject-Bridging-Header.h
│ └── ./ios/MyProject/MyProject.entitlements
├── ./ios/MyProject.xcodeproj/
├── ./ios/MyProject.xcworkspace/
└── ./ios/Other Files

AppDelegate.h

#import <Foundation/Foundation.h>
#import <React/RCTBridgeDelegate.h>
#import <RCTAppDelegate.h>
#import <UIKit/UIKit.h>
#import <Expo/Expo.h>
#import <CarPlay/CarPlay.h>

@interface AppDelegate : EXAppDelegateWrapper <RCTBridgeDelegate, UIApplicationDelegate, CPApplicationDelegate>

@property (nonatomic, strong) UIWindow *window;
@property (nonatomic, strong) UIView *rootView;
@property (nonatomic, strong) RCTBridge *bridge;

- (void)initAppFromSceneWithConnectionOptions:(UISceneConnectionOptions *)connectionOptions;

@end

AppDelegate.mm

#import <RNCarPlay.h>
#import <React/RCTBridge.h>
#import <React/RCTRootView.h>
#import <Firebase/Firebase.h>

#import "CarSceneDelegate.h"
#import "PhoneSceneDelegate.h"

#import "AppDelegate.h"

#import <React/RCTBundleURLProvider.h>
#import <React/RCTLinkingManager.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-ecd111c37e49fdd1ed6354203cd6b1e2a38cccda
[FIRApp configure];
// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions
  self.moduleName = @"main";

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

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
  return [self getBundleURL];
}

- (NSURL *)getBundleURL
{
#if DEBUG
  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"];
#else
  return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}

// Linking API
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
  return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options];
}

// Universal Links
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
  BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler];
  return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result;
}

// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
  return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}

// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
  return [super application:application didFailToRegisterForRemoteNotificationsWithError:error];
}

// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
  return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
}


- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options {
    if (@available(iOS 13.0, *)) {
        if (connectingSceneSession.role == CPTemplateApplicationSceneSessionRoleApplication) {
            UISceneConfiguration *scene = [[UISceneConfiguration alloc] initWithName:@"CarPlay" sessionRole:connectingSceneSession.role];
            scene.delegateClass = [CarSceneDelegate class];
            return scene;
        } else {
            UISceneConfiguration *scene = [[UISceneConfiguration alloc] initWithName:@"Phone" sessionRole:connectingSceneSession.role];
            scene.delegateClass = [PhoneSceneDelegate class];
            return scene;
        }
    } else {
        return nil;
    }
}

- (UIView *)rootView {
    if (!_rootView) {
        if (!self.bridge) {
            self.bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:nil];
        }
        _rootView = [[RCTRootView alloc] initWithBridge:self.bridge
                                             moduleName:@"main"
                                      initialProperties:nil];
    }
    return _rootView;
}

- (void)initAppFromSceneWithConnectionOptions:(UISceneConnectionOptions *)connectionOptions
{
    if (!self.bridge) {
        self.bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:[self connectionOptionsToLaunchOptions:connectionOptions]];
    }
    self.rootView = [self rootView];
}

- (NSDictionary *)connectionOptionsToLaunchOptions:(UISceneConnectionOptions *)connectionOptions
{
    NSMutableDictionary *launchOptions = [NSMutableDictionary dictionary];

    if (connectionOptions.notificationResponse) {
        launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey] = connectionOptions.notificationResponse.notification.request.content.userInfo;
    }

    if (connectionOptions.userActivities.count > 0) {
        NSUserActivity *userActivity = connectionOptions.userActivities.allObjects.firstObject;
        NSDictionary *userActivityDictionary = @{
            @"UIApplicationLaunchOptionsUserActivityTypeKey": userActivity.activityType,
            @"UIApplicationLaunchOptionsUserActivityKey": userActivity
        };
        launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey] = userActivityDictionary;
    }

    return launchOptions;
}

- (void)application:(UIApplication *)application didConnectCarInterfaceController:(CPInterfaceController *)interfaceController toWindow:(CPWindow *)window {
  [RNCarPlay connectWithInterfaceController:interfaceController window:window];
}

- (void)application:(nonnull UIApplication *)application didDisconnectCarInterfaceController:(nonnull CPInterfaceController *)interfaceController fromWindow:(nonnull CPWindow *)window {
  [RNCarPlay disconnect];
}

@end

CarSceneDelegate.h

#import <UIKit/UIKit.h>
#import <CarPlay/CarPlay.h>

@interface CarSceneDelegate : UIResponder <CPTemplateApplicationSceneDelegate>

@end

CarSceneDelegate

#import "CarSceneDelegate.h"
#import "AppDelegate.h"
#import <RNCarPlay.h>

@implementation CarSceneDelegate

- (void)templateApplicationScene:(CPTemplateApplicationScene *)templateApplicationScene
       didConnectInterfaceController:(CPInterfaceController *)interfaceController {
    AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate;
    [appDelegate initAppFromSceneWithConnectionOptions:templateApplicationScene.connectionOptions];
    [RNCarPlay connectWithInterfaceController:interfaceController window:templateApplicationScene.carWindow];
}

- (void)templateApplicationScene:(CPTemplateApplicationScene *)templateApplicationScene
       didDisconnectInterfaceController:(CPInterfaceController *)interfaceController {
    [RNCarPlay disconnect];
}

@end

main.m

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

int main(int argc, char * argv[]) {
  @autoreleasepool {
    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
  }
}

PhoneSceneDelegate.h

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface PhoneSceneDelegate : UIResponder <UIWindowSceneDelegate>
@property (strong, nonatomic) UIWindow *window;
@end

PhoneSceneDelegate.m

#import "PhoneSceneDelegate.h"
#import "AppDelegate.h"

@implementation PhoneSceneDelegate

- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
    AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate;
    UIWindowScene *windowScene = (UIWindowScene *)scene;
    
    [appDelegate initAppFromSceneWithConnectionOptions:connectionOptions];
    
    UIViewController *rootViewController = [[UIViewController alloc] init];
    rootViewController.view = appDelegate.rootView;
    UIWindow *window = [[UIWindow alloc] initWithWindowScene:windowScene];
    window.rootViewController = rootViewController;
    self.window = window;
    [window makeKeyAndVisible];
}

@end

To Reproduce
Try building the project with my configuration for IOS.

Expected behavior
To build successfully for IOS.

Screenshots/Videos
If applicable, add screenshots to help explain your problem.

CarPlay (please complete the following information):

  • Device: iPhone 13 mini simulator
  • OS version IOS 15.5
  • RNCarPlay version 2.4.0-beta.2

Additional context
I have the following Apple entitlements:
"com.apple.developer.carplay-audio": true,
"com.apple.developer.playable-content": true

@EliG-TA EliG-TA added the bug Something isn't working label Jul 10, 2024
@EliG-TA EliG-TA changed the title Cannot Build App On IOS Cannot Build CarPlay App On IOS Jul 10, 2024
@EliG-TA
Copy link
Author

EliG-TA commented Jul 11, 2024

Here is an update. It seems like this is a step forward in the right direction as this patch seems to allow my app to compile except I have not tested it working in runtime.

#101 (comment)

kudos to @janwiebe-jump and @casperolesen

UPDATE: 08.02.2024

Got basic Apple CarPlay working fully with much tweaking and help from above thread.

@EliG-TA EliG-TA closed this as completed Aug 2, 2024
@janwiebe-jump
Copy link
Contributor

@EliG-TA could you please share your code? I am running into issues with Expo SDK 51, I keep getting build errors and a black screen.

@EliG-TA
Copy link
Author

EliG-TA commented Aug 29, 2024

@janwiebe-jump Hey, my team is using Expo 50. Nearly all of the code I posted above was loosely adapted from this commit. Since it is company code, I am advised against sharing code. If you have any specific questions, I can try to answer to the best of my ability. You can also share your code and I can give you suggestions.

@EliG-TA
Copy link
Author

EliG-TA commented Sep 18, 2024

@janwiebe-jump You mentioned in #101 (comment) that you got it working. If so, that is amazing! I only got the CarPlay app to work when the phone app is open. Would you be able to point me in the right direction on how to get CarPlay to work standalone as mentioned in facebook/react-native#46211? My code is currently relying on "react-native": "0.73.6" and as mentioned in my previous comment, "expo": "^50.0.7". Also, is there any code in specific that you still want me to share with you? I just came to the realization that I may share some redacted code.

@janwiebe-jump
Copy link
Contributor

I have just updated my plugin to Expo SDK 51 and React Native 0.75. This allows to run the CarPlay app without running on the phone, except when the expo-dev-client is installed (only in development builds)
https://github.com/janwiebe-jump/expo-carplay-plugin

@EliG-TA
Copy link
Author

EliG-TA commented Sep 23, 2024

@janwiebe-jump Thank you for providing your solution. I tried implementing it along with upgrading my RN version to 75 from 73 and my Expo version to 51 from 50. On my end, it still does not fix the issue where the CarPlay app does not open if the phone app is not running. Also, I am trying to get it to work on the simulator with the expo-dev-client. Do you have any insights for these issues?

@EliG-TA
Copy link
Author

EliG-TA commented Sep 23, 2024

Speaking of which, maybe this expo/expo#30612 can shed some light.

@janwiebe-jump
Copy link
Contributor

Hey @EliG-TA did you try a release build?
On release builds, e.g. a production or preview build with EAS, I get the carplay app working without the phone app.
On release builds it looks like the dev client does not kick in, so the carplay app boots fine.

During development the dev-client seems to stall the boot of the carplay app. As soon as the phone app is booted, the carplay app continues.

@EliG-TA
Copy link
Author

EliG-TA commented Sep 23, 2024

Thanks @janwiebe-jump. I tried release build and it didn't change anything. What is the exact version of RN 75 and Expo 51 are you using? Also, do you think setting bridgeless mode or the new architecture to true would make a difference?

UPDATE 9.24.2024: With dev-client it is actually working for me albeit laggy. I used it to glance at the simulator logs to see why the CarPlay app either shows a blank screen or crashes when entering without the phone app running (depending on whether the dev-client is in use or not). Anyways, I found that the CarPlay scene tries to initiate the code and just exits immediately following. Here I have attached relevant logs and code snippets.

AppDelegate.mm

@implementation CarSceneDelegate

- (void)templateApplicationScene:(CPTemplateApplicationScene *)templateApplicationScene
   didConnectInterfaceController:(CPInterfaceController *)interfaceController
{
  NSLog(@"Entering CarScene");
  AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
  
  [appDelegate initAppFromScene: nil];
  
  [RNCarPlay connectWithInterfaceController:interfaceController window:templateApplicationScene.carWindow];
  NSLog(@"Exiting CarScene");
}  

CarSceneDelegate.mm

- (BOOL)initAppFromScene:(UISceneConnectionOptions *)connectionOptions {
  NSLog(@"init app from scene 1");
    // If bridge has already been initiated by another scene, there's nothing to do here
    if (self.bridge != nil) {
      NSLog(@"init app from scene 2");
        return NO;
    }

    if (self.bridge == nil) {
      NSLog(@"init app from scene 3");
      // This is broken in React Native < 0.76, so we call the implementation of the method manually
      // https://github.com/facebook/react-native/issues/44329
      // RCTAppSetupPrepareApp([UIApplication sharedApplication], self.turboModuleEnabled);
      
      // # BEGIN OF RCTAppSetupPrepareApp
      RCTEnableTurboModule(self.turboModuleEnabled);
      NSLog(@"init app from scene 4");
      #if DEBUG
        // Disable idle timer in dev builds to avoid putting application in background and complicating
        // Metro reconnection logic. Users only need this when running the application using our CLI tooling.
        [UIApplication sharedApplication].idleTimerDisabled = YES;
      #endif
      // # END OF RCTAppSetupPrepareApp
      NSLog(@"init app from scene 5");
      self.rootViewFactory = [self createRCTRootViewFactory];
    }
  NSLog(@"init app from scene 6");
    NSDictionary * initProps = [self prepareInitialProps];
  NSLog(@"init app from scene 7");
    self.rootView = [self.rootViewFactory viewWithModuleName:self.moduleName initialProperties:initProps launchOptions:[self connectionOptionsToLaunchOptions:connectionOptions]];
  NSLog(@"init app from scene 8");
    self.rootView.backgroundColor = [UIColor blackColor];
  NSLog(@"init app from scene 9");
    return YES;
}

LOGS

default 11:41:59.009689-0400 Project Entering CarScene
default 11:41:59.009802-0400 Project init app from scene 1
default 11:41:59.009908-0400 Project init app from scene 3
default 11:41:59.010004-0400 Project init app from scene 4
default 11:41:59.010097-0400 Project init app from scene 5
default 11:41:59.010780-0400 Project init app from scene 6
default 11:41:59.010950-0400 Project init app from scene 7
default 11:41:59.038130-0400 Project init app from scene 8
default 11:41:59.050131-0400 Project init app from scene 9
default 11:41:59.050301-0400 Project Exiting CarScene

VERDICT

I bet if the phone app were to be launched programmatically when entering the CarScene, then that would fix the issue because my two scenes communicate with each other. Another thing to keep in mind, is how to switch scenes on a device that does not support multiple scenes. How can one accomplish this?

UPDATE 9.26.2024: After much consideration, I don't believe it is so feasible to launch a JS bundled phone app from a CarPlay app. It seems like the phone scene and car scene should be loosely coupled. Therefore, unless anyone has any better ideas, the only solution would be to call all the CarPlay template methods and also retrieve all the necessary data in Objective-C. This seems to be related to #158 (comment)

Just to cover all bases, would this comment help solve the issue @DanielKuhn?

@DanielKuhn
Copy link
Contributor

In my opinion The CarPlay app and phone app should not be coupled at all. It is the very purpose of CarPlay that you do not need to fiddle around with your phone while driving, therefore all features of a CarPlay app need to be working standalone. This is also misleading in the example app of this project, where the phone app navigates to screens based on selections made in CarPlay and vice versa.

For this purpose I wrote the standalone-example which outlines launching a react native app headlessly (i.e. not launching on phone, not appearing in app switcher, etc.) and only interacting with the Car Scene.
In this headless scenario you can still use hooks (for example to fetch data via useQuery or whichever framework you're using), handle state, asf. - you just don't "render" anything in react native but only interact with the templates from this project.

The comment you referred to only concerns how to correctly wire linking in a scenes-based setup and has nothing to do with CarPlay.

@EliG-TA
Copy link
Author

EliG-TA commented Oct 15, 2024

@DanielKuhn Thank you for the insightful response. I have been trying to follow the standalone-example to the point. It's just that in my case, no JS code seems to be accessible/running when the app on the phone is terminated. I cloned https://github.com/janwiebe-jump/expo-carplay-plugin so that's my Obj-C code. Then, since I only got a blank car app screen when in standalone, I tried using your patch for exposing the createRCTRootViewFactory function and I get the error: "AppController.sharedInstace was called before the module was initialized". Therefore, I tried using EXAppDelegateWrapper's createRCTRootViewFactory instead and the app compiles and runs great, although the screen remains blank in standalone mode. There is some hint in this implementation from the MacOS Console application that might be of use:

When going through exclusively CarSceneDelegate,

ExpoUpdatesReactDelegateHandler throws fatal error:

"Cannot find the current window."

This comes from its function:

private func getWindow() -> UIWindow {
    guard let window = UIApplication.shared.windows.filter(\.isKeyWindow).first ?? UIApplication.shared.delegate?.window as? UIWindow else {
      fatalError("Cannot find the current window.")
    }
    return window
  }

I personally find this challenging to comprehend as @janwiebe-jump told me his code works flawlessly at least when using a development build which is precisely what I am using. The CarPlay screen shows blank when using expo-dev-client as well. In order to be completely clear, here is a rough layout of my JS side.

index.js (JS app entry point)

import { registerRootComponent } from "expo";

import App from "./App";
import { AppRegistry } from "react-native";
import CarCode from "./carplay";

// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);

AppRegistry.registerComponent("CarPlayApp", () => CarCode);

CarCode.js pseudocode

class CarCode {
  private listeners: any[] = [];

  async initialize() {
    console.log("Initializing CarCode");
    try {
      const dispatch = someStore.dispatch;
      await this.runInitialActions(dispatch);
      this.setupCarPlay();
    } catch (e) {
      console.error("Error initializing CarCode:", e);
    }
  }

  private async runInitialActions(dispatch: any) {
    console.log("Starting runInitialActions");
    try {
     SomeDispatchFunctions()
    } catch (error) {
      console.error("Error in runInitialActions:", error);
    }
  }

  private setupCarPlay() {
    CarPlay.registerOnConnect(this.onConnect);
    CarPlay.registerOnDisconnect(this.onDisconnect);
  }

  private async onConnect() {
    console.log("### CarPlay connected");
    try {
      CarPlay.setRootTemplate(someTemplate);
    } catch (error) {
      console.error("Error in onConnect:", error);
    }
  }

  private onDisconnect() {
    console.log("### CarPlay disconnected");
    CarPlay.dismissTemplate();
  }

  cleanup() {
    this.listeners.forEach((listener) => listener.remove());
      CarPlay.unregisterOnConnect(this.onConnect);
      CarPlay.unregisterOnDisconnect(this.onDisconnect);
  }
}

export default CarCode;

Please, any help would really be appreciated. 🙏 Hopefully this thread can help others as well who may experiencing similar issues.

@EliG-TA EliG-TA changed the title Cannot Build CarPlay App On IOS Standalone in iOS not loading up JS template code. Oct 15, 2024
@EliG-TA EliG-TA reopened this Oct 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants