Skip to content
This repository was archived by the owner on Feb 4, 2026. It is now read-only.

Latest commit

 

History

History
953 lines (712 loc) · 24.8 KB

File metadata and controls

953 lines (712 loc) · 24.8 KB

CarPlay with React Native

Welcome to CarPlay Development!

Begin your journey with the App Programming Guidelines for CarPlay, a comprehensive 50-page manual by Apple detailing the essentials for CarPlay apps.

For additional details while developing or contributing, refer to the CarPlay Documentation.

🚀 Quickstart: Utilize the simulator to test CarPlay capabilities without waiting for Apple's entitlement approval.

🔑 Entitlements: To deploy on a device or distribute via App Store Connect or TestFlight, obtain a CarPlay entitlement here. The approval duration varies, and participation in the MFi program may expedite the process. Incorporate the entitlement into your app's provisioning profile in Xcode.

🖥 Simulator: In Xcode, navigate to the Simulator window, choose IO > External Displays > CarPlay to launch the CarPlay simulator.

Important:

Ensure your Entitlements.plist within the iOS/ directory contains the correct entitlement key, whether for simulation or actual deployment.

Installing

You need to convert your project to using Scenes, as this is the standard when managing multiple windows in iOS 13+. This is a requirement for CarPlay apps.

0. Disclaimer

This has been proven to work on Expo SDK 53 without CNG. Your results may vary depending on your environment, but you should get an idea of the required steps anyway. :)

1. Add your PhoneScene

This is where your app will run on the phone. Make sure to provide the proper Swift header on the YOURAPP-Swift import!

PhoneScene.h

//
//  PhoneScene.h
//  ABRP
//
//  Created by Manuel Auer on 17.10.24.
//

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <React/RCTRootView.h>
#import <React/RCTLinkingManager.h>
#import "LaunchURLStorage.h"
#import "YOURAPP-Swift.h"

@interface PhoneSceneDelegate : UIResponder <UIWindowSceneDelegate>

@property (strong, nonatomic) UIWindow *window;

@end

PhoneScene.m

//
//  PhoneScene.m
//  ABRP
//
//  Created by Manuel Auer on 17.10.24.
//

#import "PhoneScene.h"
#import "ExpoScreenOrientation-Swift.h"

@implementation PhoneSceneDelegate

- (void)scene:(UIScene *)scene
willConnectToSession:(UISceneSession *)session
     options:(UISceneConnectionOptions *)connectionOptions {

   if (![session.role isEqualToString:UIWindowSceneSessionRoleApplication]) {
        return;
    }

    AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate;
    if (!appDelegate) {
        return;
    }

    if (![scene isKindOfClass:[UIWindowScene class]]) {
        return;
    }

    UIWindowScene *windowScene = (UIWindowScene *)scene;
    self.window = [[UIWindow alloc] initWithWindowScene:windowScene];
    self.window.rootViewController = appDelegate.window.rootViewController;
    [[ScreenOrientationRegistry shared] updateCurrentScreenOrientation];
  
    [self.window makeKeyAndVisible];

    UIOpenURLContext *urlContext = connectionOptions.URLContexts.allObjects.firstObject;
    if (urlContext) {
        // Linking API -> app isn’t running
        [LaunchURLStorage.shared setLaunchURL:urlContext.URL];
    }

    for (NSUserActivity *userActivity in connectionOptions.userActivities) {
        if (userActivity.webpageURL) {
            // Universal Links -> app isn’t running
            [LaunchURLStorage.shared setLaunchURL:userActivity.webpageURL];
            break;
        }
    }
}

// Linking API -> after launch
- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
    UIOpenURLContext *urlContext = URLContexts.allObjects.firstObject;
    if (!urlContext) {
        return;
    }

    NSURL *url = urlContext.URL;
    NSDictionary<UIApplicationOpenURLOptionsKey, id> *options = @{
        UIApplicationOpenURLOptionsSourceApplicationKey: urlContext.options.sourceApplication ?: @"",
        UIApplicationOpenURLOptionsAnnotationKey: urlContext.options.annotation ?: @""
    };

    [RCTLinkingManager application:UIApplication.sharedApplication openURL:url options:options];
}

// Universal Links -> after launch
- (void)scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)userActivity {
    AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate;
    if (!appDelegate) {
        return;
    }
  
    [RCTLinkingManager application:UIApplication.sharedApplication continueUserActivity:userActivity restorationHandler:^(NSArray * _Nullable _) {}];
    [appDelegate application:UIApplication.sharedApplication continueUserActivity:userActivity restorationHandler:^(NSArray * _Nullable _) {}];
}

@end

2. Add your CarScene

This is where your app will run on CarPlay.

CarScene.h

//
//  CarScene.h
//  ABRP
//
//  Created by Manuel Auer on 17.10.24.
//

#import <CarPlay/CarPlay.h>
#import <Foundation/Foundation.h>
#import "RNCarPlay.h"

@interface CarSceneDelegate : UIResponder <CPTemplateApplicationSceneDelegate>

@end

CarScene.m

//
//  CarScene.m
//  ABRP
//
//  Created by Manuel Auer on 17.10.24.
//

#import "CarScene.h"

@implementation CarSceneDelegate

- (void)templateApplicationScene:(CPTemplateApplicationScene *)templateApplicationScene
     didConnectInterfaceController:(CPInterfaceController *)interfaceController {
    [RNCarPlay connectWithInterfaceController:interfaceController window:templateApplicationScene.carWindow];
}

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

- (void)sceneDidEnterBackground:(UIScene *)scene {
  [RNCarPlay stateChanged:false];
}

- (void)sceneWillEnterForeground:(UIScene *)scene {
  [RNCarPlay stateChanged:true];
}

@end

3. Add your ClusterScene

This is what will run on the cars cluster screen (speedometer)

ClusterScene.h

//
//  ClusterScene.h
//  ABRP
//
//  Created by Manuel Auer on 07.11.24.
//

#import <CarPlay/CarPlay.h>
#import "RNCarPlay.h"

API_AVAILABLE(ios(15.4))
@interface ClusterSceneDelegate : UIResponder <CPTemplateApplicationInstrumentClusterSceneDelegate>

@property (nonatomic, strong) NSString *clusterId;

- (instancetype)init;

@end

ClusterScene.m

//
//  ClusterScene.m
//  ABRP
//
//  Created by Manuel Auer on 07.11.24.
//

#import "ClusterScene.h"

@implementation ClusterSceneDelegate

- (instancetype)init {
  self = [super init];
  if (self) {
    self.clusterId = [[NSUUID UUID] UUIDString];
  }
  return self;
}

- (void)templateApplicationInstrumentClusterScene:
            (CPTemplateApplicationInstrumentClusterScene *)
                templateApplicationInstrumentClusterScene
            didConnectInstrumentClusterController:
                (CPInstrumentClusterController *)instrumentClusterController {
  UIUserInterfaceStyle contentStyle = templateApplicationInstrumentClusterScene.contentStyle;
  [RNCarPlay connectWithInstrumentClusterController:instrumentClusterController contentStyle:contentStyle clusterId:self.clusterId];
}

- (void)templateApplicationInstrumentClusterScene:(CPTemplateApplicationInstrumentClusterScene *)templateApplicationInstrumentClusterScene didDisconnectInstrumentClusterController:(CPInstrumentClusterController *)instrumentClusterController {
  [RNCarPlay disconnectFromInstrumentClusterController:self.clusterId];
}

- (void)contentStyleDidChange:(UIUserInterfaceStyle)contentStyle {
  [RNCarPlay clusterContentStyleDidChange:contentStyle clusterId:self.clusterId];
}

- (void)sceneDidEnterBackground:(UIScene *)scene {
  [RNCarPlay clusterStateChanged:self.clusterId isVisible:false];
}

- (void)sceneWillEnterForeground:(UIScene *)scene {
  [RNCarPlay clusterStateChanged:self.clusterId isVisible:true];
}

@end

4. Add your DashboardScene

This is what will run on the CarPlay Dashboard next to other widgets like Calendar, Music...

DashboardScene.h

//
//  DashboardScene.h
//  ABRP
//
//  Created by Manuel Auer on 17.10.24.
//

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

@interface DashboardSceneDelegate : UIResponder <CPTemplateApplicationDashboardSceneDelegate>

@end

DashboardScene.m

//
//  DashboardScene.m
//  ABRP
//
//  Created by Manuel Auer on 17.10.24.
//

#import "DashboardScene.h"

@implementation DashboardSceneDelegate

- (void)templateApplicationDashboardScene:(CPTemplateApplicationDashboardScene *)templateApplicationDashboardScene
                didConnectDashboardController:(CPDashboardController *)dashboardController
                                     toWindow:(UIWindow *)window {
    [RNCarPlay connectWithDashboardController:dashboardController window:window];
}

- (void)templateApplicationDashboardScene:(CPTemplateApplicationDashboardScene *)templateApplicationDashboardScene
            didDisconnectDashboardController:(CPDashboardController *)dashboardController
                                   fromWindow:(UIWindow *)window {
    [RNCarPlay disconnectFromDashbaordController];
}

- (void)sceneDidEnterBackground:(UIScene *)scene {
  [RNCarPlay dashboardStateChanged:false];
}

- (void)sceneWillEnterForeground:(UIScene *)scene {
  [RNCarPlay dashboardStateChanged:true];
}

@end

5. Add RCTLinkingManager patch

this makes sure getInitialURL works as expected

RCTLinkingManager+Custom.mm

//
//  RCTLinkingManager+Custom.mm
//  this overrides the original RCTLinkingManager getInitialURL to provide the url the app was started with from a scene delegate
//  Created by Manuel Auer on 04.10.24.
//

#import <React/RCTLinkingManager.h>
#import "LaunchURLStorage.h"
#import <React/RCTBridge.h>
#import <React/RCTUtils.h>

@implementation RCTLinkingManager (Custom)

RCT_EXPORT_METHOD(getInitialURL:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
    NSURL *initialURL = [LaunchURLStorage shared].launchURL;

    if (initialURL) {
        resolve(RCTNullIfNil(initialURL.absoluteString));
        return;
    }
  
    // Fallback to the original implementation
    if (self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey]) {
        initialURL = self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey];
    } else {
        NSDictionary *userActivityDictionary =
            self.bridge.launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey];
        if ([userActivityDictionary[UIApplicationLaunchOptionsUserActivityTypeKey] isEqual:NSUserActivityTypeBrowsingWeb]) {
            initialURL = ((NSUserActivity *)userActivityDictionary[@"UIApplicationLaunchOptionsUserActivityKey"]).webpageURL;
        }
    }
    
    resolve(RCTNullIfNil(initialURL.absoluteString));
}

@end

6. Add LaunchURLStorage

required for the above RCTLinkingManager patch

LaunchURLStorage.h

//
//  LaunchURLStorage.h
//  ABRP
//
//  Created by Manuel Auer on 04.10.24.
//
#import <Foundation/Foundation.h>

@interface LaunchURLStorage : NSObject

@property (nonatomic, strong) NSURL *launchURL;

+ (instancetype)shared;

@end

LaunchURLStorage.m

//
//  LaunchURLStorage.m
//  ABRP
//
//  Created by Manuel Auer on 04.10.24.
//

#import "LaunchURLStorage.h"

@implementation LaunchURLStorage

+ (instancetype)shared {
    static LaunchURLStorage *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

@end

7. Add Scene Manifest to Info.plist

adjust to your needs, this is the fully featured CarPlay, Dashboard & Cluster screen setup.

ios/App/Info.plist

    <key>UIApplicationSceneManifest</key>
    <dict>
      <key>CPSupportsDashboardNavigationScene</key>
      <true/>
      <key>CPSupportsInstrumentClusterNavigationScene</key>
      <true/>
      <key>UIApplicationSupportsMultipleScenes</key>
      <true/>
      <key>UISceneConfigurations</key>
      <dict>
        <key>CPTemplateApplicationDashboardSceneSessionRoleApplication</key>
        <array>
          <dict>
            <key>UISceneClassName</key>
            <string>CPTemplateApplicationDashboardScene</string>
            <key>UISceneConfigurationName</key>
            <string>Dashboard</string>
            <key>UISceneDelegateClassName</key>
            <string>DashboardSceneDelegate</string>
          </dict>
        </array>
        <key>CPTemplateApplicationInstrumentClusterSceneSessionRoleApplication</key>
        <array>
          <dict>
            <key>UISceneClassName</key>
            <string>CPTemplateApplicationInstrumentClusterScene</string>
            <key>UISceneConfigurationName</key>
            <string>Cluster</string>
            <key>UISceneDelegateClassName</key>
            <string>ClusterSceneDelegate</string>
          </dict>
        </array>
        <key>CPTemplateApplicationSceneSessionRoleApplication</key>
        <array>
          <dict>
            <key>UISceneClassName</key>
            <string>CPTemplateApplicationScene</string>
            <key>UISceneConfigurationName</key>
            <string>CarPlay</string>
            <key>UISceneDelegateClassName</key>
            <string>CarSceneDelegate</string>
          </dict>
        </array>
        <key>UIWindowSceneSessionRoleApplication</key>
        <array>
          <dict>
            <key>UISceneClassName</key>
            <string>UIWindowScene</string>
            <key>UISceneConfigurationName</key>
            <string>Phone</string>
            <key>UISceneDelegateClassName</key>
            <string>PhoneSceneDelegate</string>
          </dict>
        </array>
      </dict>
    </dict>

Entitlement matrix

List Grid T B Alert A S 🎤 ▶️ Map 🔎 POI Info 📇
com.apple.developer.carplay-audio
com.apple.developer.carplay-communication
com.apple.developer.carplay-charging
com.apple.developer.carplay-maps
com.apple.developer.carplay-parking
com.apple.developer.carplay-quick-ordering

Connect / Disconnect

Efficiently manage CarPlay connections by utilizing the connected status and on-connect/disconnect events. Ensure to check the connection state before invoking CarPlay APIs, ideally within a useEffect hook or by using a non-React function.

// react
useEffect(() => {
  CarPlay.registerOnConnect(onConnect);
  return () => {
    CarPlay.unregisterOnConnect(onConnect);
  };
});

// imperative
CarPlay.registerOnConnect(() => {
  CarPlay.setRootTemplate(/* template */);
});

Templates

Templates are used to render contents on the CarPlay screen from your app. Details of the templates supported by apple can be found in the developer guide

MapTemplate

A template that displays a navigation overlay that your app draws on the map.

Visual Previews

Map Template Map Template

Example Usage

new MapTemplate({
  component: /* react native view */ MapView,
  guidanceBackgroundColor: '#eeff00',
  onAlertActionPressed() {},
  onStartedTrip() {},
});

See more configuration options in the TypeScript Docs

Relevant Links

  • CPMapTemplate: Learn more about the capabilities and limitations of the MapTemplate in CarPlay.
  • CPMapTemplateDelegate: Understand the delegate callbacks that can be used to manage user interactions with the MapTemplate.

ListTemplate

A template that displays and manages a list of items.

Visual Previews

List Template

Example Usage

new ListTemplate({
  sections: [
    {
      header: 'Header A',
      items: [
        {
          text: 'Item 1',
        },
      ],
    },
  ],
  title: 'List Template',
  async onItemSelect() {},
});

See more configuration options in the TypeScript Docs

Relevant Links

  • CPListTemplate: Learn more about the capabilities and limitations of the ListTemplate in CarPlay.
  • CPListTemplateDelegate: Understand the delegate callbacks that can be used to manage user interactions with the ListTemplate.

InformationTemplate

A template that provides information for a point of interest, food order, parking location, or charging location.

Visual Previews

Information Template

Example Usage

new InformationTemplate({
  title: 'Information',
  items: [{ title: 'foo', detail: 'bar' }],
  actions: [{ id: 'demo', title: 'Demo' }],
  onActionButtonPressed() {},
});

See more configuration options in the TypeScript Docs

Relevant Links

  • CPInformationTemplate: Learn more about the capabilities and limitations of the InformationTemplate in CarPlay.
  • CPInformationTemplateDelegate: Understand the delegate callbacks that can be used to manage user interactions with the InformationTemplate.

GridTemplate

A template that displays and manages a grid of items.

Visual Previews

Grid Template

Example Usage

new GridTemplate({
  trailingNavigationBarButtons: [
    {
      id: 'a',
      type: 'image',
      image: require('star.jpg'),
    },
  ],
  buttons: [
    {
      id: '0',
      titleVariants: ['Item 0'],
      image: require('click.jpg'),
    },
  ],
  title: 'Grid Template',
  onButtonPressed() {},
  onBarButtonPressed() {},
});

See more configuration options in the TypeScript Docs

Relevant Links

  • CPGridTemplate: Discover the capabilities and constraints of the CPGridTemplate in CarPlay.
  • CPGridTemplateDelegate: Delve into the delegate callbacks available for handling user interactions within the CPGridTemplate.

SearchTemplate

A template that provides the ability to search for a destination and see a list of search results.

Visual Previews

Search Template

Example Usage

new SearchTemplate({
  async onSearch(query) {},
  async onItemSelect({ index }) {},
  onSearchButtonPressed() {},
});

See more configuration options in the TypeScript Docs

Relevant Links

  • CPSearchTemplate: Explore the features and limitations of the CPSearchTemplate in CarPlay.
  • CPSearchTemplateDelegate: Learn about the delegate callbacks for managing interactions within the CPSearchTemplate.

VoiceTemplate

A template that displays a voice control indicator during audio input.

This template is presented via CarPlay.presentTemplate. In order to implement voice recognition, take a look at the @react-native-voice/voice package.

Visual Previews

Voice Template

Example Usage

new VoiceControlTemplate({
  voiceControlStates: [
    {
      identifier: 'a',
      image: require('cat.jpg'),
      repeats: true,
      titleVariants: ['Searching...'],
    },
  ],
});

See more configuration options in the TypeScript Docs

Relevant Links

  • CPVoiceControlTemplate: Investigate the functionalities and boundaries of the CPVoiceControlTemplate in CarPlay.
  • CPVoiceControlTemplateDelegate: Gain insights into the delegate methods designed to handle user voice commands in the CPVoiceControlTemplate.
  • SFSpeechRecognizer: Learn about speech recognition tasks and integrations with CarPlay applications.

AlertTemplate

A template that displays a modal alert and should be presented via CarPlay.presentTemplate.

Visual Previews

Alert Template

Example Usage

new AlertTemplate({
  titleVariants: ['Hello world'],
  actions: [
    {
      id: 'ok',
      title: 'Ok',
    },
    {
      id: 'ok',
      title: 'Cancel',
    },
    {
      id: 'remove',
      title: 'Remove',
      style: 'destructive',
    },
  ],
  onActionButtonPressed() {},
});

Relevant Links

  • CPAlertTemplate: Explore the functionality of CPAlertTemplate for displaying alerts in CarPlay.
  • CPAlertTemplateDelegate: Learn about delegate methods for managing user interactions with CarPlay alerts.

ActionSheetTemplate

A template that displays a modal action sheet and should be presented via CarPlay.presentTemplate.

Visual Previews

ActionSheet Template

Example Usage

new ActionSheetTemplate({
  title: 'Example',
  message: 'This is an message for you',
  actions: [
    {
      id: 'ok',
      title: 'Ok',
    },
    {
      id: 'remove',
      title: 'Remove',
      style: 'destructive',
    },
  ],
  onActionButtonPressed() {},
});

Relevant Links

TabBarTemplate

A container template that displays and manages other templates, presenting them as tabs.

Note: This template must be set as the root template and cannot be pushed on top of other templates.

Visual Previews

TabBar Template

Example Usage

// Define tab templates
const tpl1 = new ListTemplate(/* ... */);
const tpl2 = new ListTemplate(/* ... */);

// Setup the tab container template
new TabBarTemplate({
  templates: [tpl1, tpl2],
  onTemplateSelect() {},
});

Relevant Links

  • CPTabBarTemplate: Investigate the features and usage of CPTabBarTemplate to create a tab bar interface in CarPlay.
  • CPTabBarTemplateDelegate: Explore the delegate methods for responding to tab selection events in the CPTabBarTemplate. ``

Troubleshooting

Image Size and Resolution

Quirks observed where PNG image resolutions should be specfied with scale factor of 3.0 (i.e. append with @3x) with ListTemplate image sizing suggested around 80 x 80 px per Issue #6