Kacper Szewczyk
JS Developer
Magdalena Jackiewicz
Reviewed by a tech expert

How to connect your React Native app to CarPlay?

#Sales
#Sales
#Sales
#Sales
Read this articles in:
EN
PL

Creating seamless experiences across various platforms is a crucial component of mobile app development these days. With the rising popularity of connected cars, integrating your React Native application with Apple CarPlay opens up a world of possibilities for location-based services and in-vehicle experiences.

At RST Software, we've been at the forefront of location-based application development, so we're excited to share our expertise in bridging the gap between React Native and CarPlay.

Whether you're looking to expand your skill set or to enhance your app's functionality, this guide will walk you through the process of adding new CarPlay apps when working with React Native. If you want to understand what challenges you’ll have to overcome during the process and how to solve them, this blog is for you.

What is CarPlay?

CarPlay, developed by Apple, is an in-car interface that integrates an iPhone with a vehicle's infotainment system. This technology enables users to project their smartphone's content onto the car's display and interact with their device through the vehicle's built-in controls. Popular applications for CarPlay include streaming music from platforms like Spotify, as well as various navigation services.

With each new iOS update, CarPlay's capabilities continue to expand, introducing support for new app categories. This ongoing evolution creates opportunities for developers to create third-party applications specifically tailored for in-car use. Recent additions to the CarPlay ecosystem include apps for ordering fast food and locating electric vehicle charging stations, demonstrating the platform's growing versatility and accessibility.

How does CarPlay work?

CarPlay offers developers a curated set of pre-designed interface templates for presenting application data - the complete list of these templates is available in Apple’s CarPlay Programming Guide. These templates prioritize driver safety and maintain a consistent user experience across different apps. The range of available templates is tailored to suit specific application categories, ensuring that each app type can effectively display its content while adhering to CarPlay's safety standards.

For instance, navigation applications have access to a comprehensive selection of templates, including map controls, list views, and grid layouts. This variety allows for rich, interactive experiences suited to the complexities of route planning and guidance. On the other hand, apps with simpler functionalities, such as those for ordering fast food, are limited to a more focused set of templates, streamlining the user interaction process.

It’s important to be aware of these templates, as every time you update your application, Apple will conduct a review of those changes and if your updates don’t comply with those pre-defined standards, your update will be rejected.

CarPlay also caters to niche application categories with specialized templates. A prime example is the Point of Interest template, designed specifically for services like EV charging station locators. This unique template efficiently combines map and list views, allowing users to simultaneously visualize multiple locations both geographically and in a structured list format.

Challenges with connecting CarPlay and React Native

The integration of CarPlay functionality into React Native applications is an emerging field, with the GitHub repository 'react-native-carplay' leading the way in this area. The library offers definitions to enhance your development experience. It enables the creation of most CarPlay-supported templates and provides control over the CarPlay display stack through intuitive push and pop operations.

Additionally, the package implements socket-like communication – CarPlay connection and disconnection, allowing developers to implement responsive behaviors within their applications.

When it comes to connecting a React Native application with CarPlay, there are several challenges that developers will have to overcome:

Native code configuration

Setting up react-native-carplay does require some initial native code configuration. However, once this initial setup is complete, subsequent development can be carried out entirely within the React Native environment. Detailed instructions for this setup process are available in the package's documentation.


@main
class AppDelegate: RCTAppDelegate {
  
  // declare and implement initAppFromScene function to allow to launch CarPlay and phone app independently
  func initAppFromScene(connectionOptions: UIScene.ConnectionOptions?) {
    if self.bridge != nil {
      return
    }
    let enableTM = false;
    #if RCT_NEW_ARCH_ENABLED
      enableTM = self.turboModuleEnabled
    #endif

    let application = UIApplication.shared
    RCTAppSetupPrepareApp(application, enableTM)

    if self.bridge == nil {
      self.bridge = super.createBridge(
        with: self,
        launchOptions: self.connectionOptionsToLaunchOptions(connectionOptions: connectionOptions)
      )
    }

    #if RCT_NEW_ARCH_ENABLED
      _contextContainer = UnsafeMutablePointer.allocate(capacity: 1)
      _contextContainer?.initialize(to: ContextContainer())
      _reactNativeConfig = UnsafeMutablePointer.allocate(capacity: 1)
      _reactNativeConfig?.initialize(to: EmptyReactNativeConfig())
      _contextContainer?.pointee.insert("ReactNativeConfig", _reactNativeConfig)
      self.bridgeAdapter = RCTSurfacePresenterBridgeAdapter(bridge: self.bridge, contextContainer: _contextContainer)
      self.bridge?.surfacePresenter = self.bridgeAdapter?.surfacePresenter
    #endif

    let initProps = prepareInitialProps()
    self.rootView = self.createRootView(with: self.bridge, moduleName: self.moduleName, initProps: initProps)

  }
  
   /**
    Convert ConnectionOptions to LaunchOptions
    When Scenes are used, the launchOptions param in "didFinishLaunchingWithOptions" is always null,
    and the expected data is provided through SceneDelegate's ConnectionOptions instead but in a different format
    */
    func connectionOptionsToLaunchOptions(connectionOptions: UIScene.ConnectionOptions?) -> [UIApplication.LaunchOptionsKey: Any] {
      var launchOptions: [UIApplication.LaunchOptionsKey: Any] = [:]

      if let options = connectionOptions {
        if options.notificationResponse != nil {
          launchOptions[UIApplication.LaunchOptionsKey.remoteNotification] =
            options.notificationResponse?.notification.request.content.userInfo
        }

        if !options.userActivities.isEmpty {
          let userActivity = options.userActivities.first
          let userActivityDictionary = [
            "UIApplicationLaunchOptionsUserActivityTypeKey": userActivity?.activityType as Any,
            "UIApplicationLaunchOptionsUserActivityKey": userActivity!
          ] as [String : Any];
          launchOptions[UIApplication.LaunchOptionsKey.userActivityDictionary] = userActivityDictionary
        }
      }

      return launchOptions;
    }
  
} 

Adjusting to CarPlay’s predefined UI designs

As mentioned earlier, CarPlay provides a set of predefined templates that can be used to create your CarPlay app. In practice, this means you may have to redesign your existing app to create its minimized counterpart for the in-car system. If your application is robust and complex, this redesign may prove challenging – it will potentially require you to resign from certain app functionalities or displays. Take a look at these samples:

Map settings functionality.
Navigation view.
Available map colors.

Ensuring the app runs independently

Your application must be able to run independently (continuing to operate while you use other apps), and that’s not something that CarPlay’s library supports. You’ll have to write a custom application for that.



@main
class AppDelegate: RCTAppDelegate {
  
  // declare and implement initAppFromScene function to allow to launch CarPlay and phone app independently
  func initAppFromScene(connectionOptions: UIScene.ConnectionOptions?) {
    if self.bridge != nil {
      return
    }
    let enableTM = false;
    #if RCT_NEW_ARCH_ENABLED
      enableTM = self.turboModuleEnabled
    #endif

    let application = UIApplication.shared
    RCTAppSetupPrepareApp(application, enableTM)

    if self.bridge == nil {
      self.bridge = super.createBridge(
        with: self,
        launchOptions: self.connectionOptionsToLaunchOptions(connectionOptions: connectionOptions)
      )
    }

    #if RCT_NEW_ARCH_ENABLED
      _contextContainer = UnsafeMutablePointer.allocate(capacity: 1)
      _contextContainer?.initialize(to: ContextContainer())
      _reactNativeConfig = UnsafeMutablePointer.allocate(capacity: 1)
      _reactNativeConfig?.initialize(to: EmptyReactNativeConfig())
      _contextContainer?.pointee.insert("ReactNativeConfig", _reactNativeConfig)
      self.bridgeAdapter = RCTSurfacePresenterBridgeAdapter(bridge: self.bridge, contextContainer: _contextContainer)
      self.bridge?.surfacePresenter = self.bridgeAdapter?.surfacePresenter
    #endif

    let initProps = prepareInitialProps()
    self.rootView = self.createRootView(with: self.bridge, moduleName: self.moduleName, initProps: initProps)

  }
  
   /**
    Convert ConnectionOptions to LaunchOptions
    When Scenes are used, the launchOptions param in "didFinishLaunchingWithOptions" is always null,
    and the expected data is provided through SceneDelegate's ConnectionOptions instead but in a different format
    */
    func connectionOptionsToLaunchOptions(connectionOptions: UIScene.ConnectionOptions?) -> [UIApplication.LaunchOptionsKey: Any] {
      var launchOptions: [UIApplication.LaunchOptionsKey: Any] = [:]

      if let options = connectionOptions {
        if options.notificationResponse != nil {
          launchOptions[UIApplication.LaunchOptionsKey.remoteNotification] =
            options.notificationResponse?.notification.request.content.userInfo
        }

        if !options.userActivities.isEmpty {
          let userActivity = options.userActivities.first
          let userActivityDictionary = [
            "UIApplicationLaunchOptionsUserActivityTypeKey": userActivity?.activityType as Any,
            "UIApplicationLaunchOptionsUserActivityKey": userActivity!
          ] as [String : Any];
          launchOptions[UIApplication.LaunchOptionsKey.userActivityDictionary] = userActivityDictionary
        }
      }

      return launchOptions;
    }
  
}

import Foundation
import CarPlay

class CarSceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate {
  func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene,
                                  didConnect interfaceController: CPInterfaceController) {
    guard let appDelegate = (UIApplication.shared.delegate as? AppDelegate) else { return }

    appDelegate.initAppFromScene(connectionOptions: nil)

    RNCarPlay.connect(with: interfaceController, window: templateApplicationScene.carWindow)
  }

  func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didDisconnectInterfaceController interfaceController: CPInterfaceController) {
    RNCarPlay.disconnect()
  }
}
<key>UIApplicationSceneManifest</key>
<dict>
  <key>UIApplicationSupportsMultipleScenes</key>
  <true/>
  <key>UISceneConfigurations</key>
  <dict>
    <key>CPTemplateApplicationSceneSessionRoleApplication</key>
    <array>
      <dict>
        <key>UISceneClassName</key>
        <string>CPTemplateApplicationScene</string>
        <key>UISceneConfigurationName</key>
        <string>CarPlay</string>
        <key>UISceneDelegateClassName</key>
        <string>$(PRODUCT_MODULE_NAME).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>$(PRODUCT_MODULE_NAME).PhoneSceneDelegate</string>
      </dict>
    </array>
  </dict>
</dict>

// You need to convert your project to using Scenes, as this is the standard when managing multiple windows in iOS 13+. 

class PhoneSceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?
  func scene(
    _ scene: UIScene, willConnectTo session: UISceneSession,
    options connectionOptions: UIScene.ConnectionOptions
  ) {
    guard let appDelegate = (UIApplication.shared.delegate as? AppDelegate) else { return }
    guard let windowScene = (scene as? UIWindowScene) else { return }

    appDelegate.initAppFromScene(connectionOptions: connectionOptions)

    let rootViewController = UIViewController()
    rootViewController.view = appDelegate.rootView
    let window = UIWindow(windowScene: windowScene)
    window.rootViewController = rootViewController
    self.window = window
    window.makeKeyAndVisible()
  }
  // Implement other functions that you need to support on native side in this file
}
<dict>
  <key>com.apple.developer.carplay-maps</key> <!-- Or any other type of carplay -->
  <true/>
</dict>

Connecting your React Native app to CarPlay – a step-by-step process

Step 1: Obtain Apple’s CarPlay entitlements

When it comes to publishing a CarPlay-enabled application on the App Store, developers must obtain specific entitlements from Apple. Without the required certification from Apple, your app simply won’t be visible to CarPlay.

For development and testing purposes, however, Apple provides a built-in CarPlay simulator. The latest iOS 15 simulator offers a comprehensive set of features that closely mimics the behavior of a physical CarPlay unit, providing an excellent environment for testing and refinement.

Note: By default, the CarPlay simulator works only in 480x360 px. If you want to change the dimensions of the screen, use this command in the terminal:


defaults write com.apple.iphonesimulator CarPlayExtraOptions -bool YES

Step 2: Create a Carplay layer for your RN application

This step involves identifying the screens the CarPlay layer of the app will use and creating transitions between them. You’ll have to select the screens and templates to be used by the CarPlay layer of the app.

Here's a simple implementation of showing map templates in CarPlay on closed applications.


import React from 'react';
import { useCarPlay } from './src/hooks/useCarPlay';

const App = () => {

  useCarPlay();

  return (
  ...
  );
};

export default App;

import { useEffect } from 'react';
import { CarPlay } from 'react-native-carplay';
import { PERMISSIONS, requestMultiple } from 'react-native-permissions';
import { showBasicMap } from '../utils/carPlay';
import { isAndroid } from '../utils/device';

const getBasicMapConfig = (): MapTemplateConfig => ({
  id: ,
  component: ,
  hidesButtonsWithNavigationBar: true,
  leadingNavigationBarButtons: [
    // define buttons that appear on the top left size of the map
    // maximum 2 buttons possible
  ],
  trailingNavigationBarButtons: [
    // define buttons that appear on the top right size of the map
    // maximum 2 buttons possible
  ],
  mapButtons: [
    // define buttons that appear on the bottom right size of the map
    // maximum 4 buttons possible
  ],
  onMapButtonPressed: (e) => {
    // define callback function after clicking map button
  },
  onBarButtonPressed:  (e) => {
    // define callback function after clicking on trailing or leading buttons
  },
});

const showBasicMap = async () => {
  if (!CarPlay.connected) {
    return;
  }
  const mapTemplate = new MapTemplate(getBasicMapConfig());
  CarPlay.setRootTemplate(mapTemplate, true);
};


const useCarPlay = () => {

  const onCarPlayConnect = async () => {
    await showBasicMap();
    // Initialize any events that you want to subscribe to on application side 
    // You can define listeners both on Phone and CarPlay side. 
    // Use listeners and CarPlay.emitter.emit({}) method to send messages between both layers
    CarPlay.emitter.addListener(
      ,
      ,
    );

    // Request permissions here if you want to support voice control
    requestMultiple([
      PERMISSIONS.IOS.SPEECH_RECOGNITION,
      PERMISSIONS.IOS.MICROPHONE,
    ]);
  };

  useEffect(() => {
    CarPlay.registerOnConnect(onCarPlayConnect);
    CarPlay.registerOnDisconnect(() => {
      // Disable listeners on disconnect - they are not unmounted on disconnect
      CarPlay.emitter.removeAllListeners();
    });

    return () => {
    // make sure to cleanup on closing the app
      CarPlay.unregisterOnConnect(onCarPlayConnect);
      CarPlay.emitter.removeAllListeners();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
};

// If you want to disable Android Auto you can do sth like that
const hookMock = () => {};

const hook = isAndroid ? hookMock : useCarPlay;
export { hook as useCarPlay };

Step 3: Define the synchronization frequency

Developers working on connecting their React Native apps to CarPlay have to pay attention to the frequency of synchronizations between the two app layers. CarPlay has rather poor memory management, so you’ll have to ensure the connection and synchronization between the two layers of the app doesn’t happen too often, as that may overload the CarPlay system, causing your app to crash.

Here’s what you need to know to avoid this:

  • You may have to write a custom implementation that will be taking care of clearing the cache.
  • Your application must be able to run independently, and that’s not something that CarPlay’s library supports. You’ll have to write a custom application for that.
  • Always unsubscribe from any existing listeners if they are no longer needed. You can see an example of this here:

import React from 'react';
import { useCarPlay } from './src/hooks/useCarPlay';

const App = () => {

  useCarPlay();

  return (
  ...
  );
};

export default App;

import { useEffect } from 'react';
import { CarPlay } from 'react-native-carplay';
import { PERMISSIONS, requestMultiple } from 'react-native-permissions';
import { showBasicMap } from '../utils/carPlay';
import { isAndroid } from '../utils/device';

const getBasicMapConfig = (): MapTemplateConfig => ({
  id: ,
  component: ,
  hidesButtonsWithNavigationBar: true,
  leadingNavigationBarButtons: [
    // define buttons that appear on the top left size of the map
    // maximum 2 buttons possible
  ],
  trailingNavigationBarButtons: [
    // define buttons that appear on the top right size of the map
    // maximum 2 buttons possible
  ],
  mapButtons: [
    // define buttons that appear on the bottom right size of the map
    // maximum 4 buttons possible
  ],
  onMapButtonPressed: (e) => {
    // define callback function after clicking map button
  },
  onBarButtonPressed:  (e) => {
    // define callback function after clicking on trailing or leading buttons
  },
});

const showBasicMap = async () => {
  if (!CarPlay.connected) {
    return;
  }
  const mapTemplate = new MapTemplate(getBasicMapConfig());
  CarPlay.setRootTemplate(mapTemplate, true);
};


const useCarPlay = () => {

  const onCarPlayConnect = async () => {
    await showBasicMap();
    // Initialize any events that you want to subscribe to on application side 
    // You can define listeners both on Phone and CarPlay side. 
    // Use listeners and CarPlay.emitter.emit({}) method to send messages between both layers
    CarPlay.emitter.addListener(
      ,
      ,
    );

    // Request permissions here if you want to support voice control
    requestMultiple([
      PERMISSIONS.IOS.SPEECH_RECOGNITION,
      PERMISSIONS.IOS.MICROPHONE,
    ]);
  };

  useEffect(() => {
    CarPlay.registerOnConnect(onCarPlayConnect);
    CarPlay.registerOnDisconnect(() => {
      // Disable listeners on disconnect - they are not unmounted on disconnect
      CarPlay.emitter.removeAllListeners();
    });

    return () => {
    // make sure to cleanup on closing the app
      CarPlay.unregisterOnConnect(onCarPlayConnect);
      CarPlay.emitter.removeAllListeners();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
};

// If you want to disable Android Auto you can do sth like that
const hookMock = () => {};

const hook = isAndroid ? hookMock : useCarPlay;
export { hook as useCarPlay };
  • Minimize JavaScript animations in components rendered on the CarPlay interface. As a best practice, avoid refreshing components more frequently than once per minute.
  • When creating templates, add a timestamp or any other unique identifier to the id. This will help manage events effectively, even if there are issues with listener cleanup.

Step 4: Call out the first screen

This final step – calling out the app launch screen in CarPlay – is a test to whether the implementation has worked or not.

Now, you can try pushing other templates onto your scene, such as a grid or list template. Experiment with triggering template changes based on navigation actions from the phone side or upon receiving an event.

Need support with connecting your React Native app with CarPlay?

We’re hoping that you’ve found this guide informative and that the information outlined helped you with your app development goals. If at any point, however, you find yourself in doubt with regards to this process, feel free to contact us at hi@rst.software and we’ll be happy to answer your questions.

People also ask

No items found.
Want more posts from the author?
Read more

Want to read more?

Mobile

React Native vs NativeScript: which technology to pick for mobile development in 2021

Two JavaScript-based technologies for cross-platform development, but which to choose? Let's compare React Native and NativeScript.
Mobile

25+ in-depth examples of React Native apps in 2021 [Updated: May 25th, 2021]

I’ve compiled a long list of well-known companies that decided to use React Native in their apps and stuck with it. Read on.
Mobile

5 benefits of React Native or why you should go cross-platform, not native

Would you like to build a set of iOS and Android apps that share as much as 99% of their code? Doable with React Native.
No results found.
There are no results with this criteria. Try changing your search.
en