carplay

$npx mdskill add dpearson2699/swift-ios-skills/carplay

Build CarPlay-enabled apps using the CarPlay framework.

  • Create navigation, audio, communication, EV charging, parking, or food ordering apps for the car display.
  • Uses CPTemplateApplicationScene, CPInterfaceController, and system CPTemplate navigation.
  • Selects appropriate templates like CPListTemplate, CPMapTemplate, or CPNowPlayingTemplate based on app category.
  • Delivers template-based UI through CarPlay Simulator or real vehicle integration.

SKILL.md

.github/skills/carplayView on GitHub ↗
---
name: carplay
description: "Build CarPlay-enabled apps using the CarPlay framework. Use when creating navigation, audio, communication, EV charging, parking, or food ordering apps for the car display, working with CPTemplateApplicationScene, CPInterfaceController template hierarchies, CPListTemplate, CPMapTemplate, CPNowPlayingTemplate, configuring CarPlay entitlements, or integrating with CarPlay Simulator for testing."
---

# CarPlay

Build category-entitled, template-based CarPlay apps for the vehicle display.
Scope: Swift 6.3, iOS 26+.

See [references/carplay-patterns.md](references/carplay-patterns.md) for extended patterns including full
navigation sessions, dashboard scenes, and advanced template composition.

Scope boundary: full CarPlay framework apps use category entitlements,
`CPTemplateApplicationScene`, `CPTemplateApplicationSceneDelegate`,
`CPInterfaceController`, and system `CPTemplate` navigation. CarPlay-visible
WidgetKit widgets and ActivityKit Live Activities are separate system
experiences; route their implementation to those domains while keeping
CarPlay-specific validation here.

## Contents

- [Entitlements and Setup](#entitlements-and-setup)
- [Scene Configuration](#scene-configuration)
- [Templates Overview](#templates-overview)
- [Navigation Apps](#navigation-apps)
- [Audio Apps](#audio-apps)
- [Communication Apps](#communication-apps)
- [Point of Interest Apps](#point-of-interest-apps)
- [Testing with CarPlay Simulator](#testing-with-carplay-simulator)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)

## Entitlements and Setup

Request the category-specific entitlement from
[Apple's CarPlay entitlement form](https://developer.apple.com/contact/carplay),
accept the addendum, then provision the approved category key below.

### Entitlement Keys by Category

| Entitlement | Category |
|---|---|
| `com.apple.developer.carplay-audio` | Audio |
| `com.apple.developer.carplay-communication` | Communication |
| `com.apple.developer.carplay-maps` | Navigation |
| `com.apple.developer.carplay-charging` | EV Charging |
| `com.apple.developer.carplay-parking` | Parking |
| `com.apple.developer.carplay-quick-ordering` | Quick Food Ordering |

### Project Configuration

1. Update the App ID in the developer portal under Additional Capabilities.
2. Generate a new provisioning profile for the updated App ID.
3. In Xcode, disable automatic signing and import the CarPlay provisioning profile.
4. Add an `Entitlements.plist` with the entitlement key set to `true`.
5. Set Code Signing Entitlements build setting to the `Entitlements.plist` path.

### Key Types

| Type | Role |
|---|---|
| `CPTemplateApplicationScene` | UIScene subclass for the CarPlay display |
| `CPTemplateApplicationSceneDelegate` | Scene connect/disconnect lifecycle |
| `CPInterfaceController` | CarPlay-provided controller for setting the root template and pushing, presenting, or popping templates |
| `CPTemplate` | Abstract base for all CarPlay templates |
| `CPSessionConfiguration` | Vehicle display limits and content style |

## Scene Configuration

Declare the CarPlay scene in `Info.plist` and implement
`CPTemplateApplicationSceneDelegate` to respond when CarPlay connects.

### Info.plist Scene Manifest

```plist
<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>CarPlaySceneConfiguration</string>
                <key>UISceneDelegateClassName</key>
                <string>$(PRODUCT_MODULE_NAME).CarPlaySceneDelegate</string>
            </dict>
        </array>
    </dict>
</dict>
```

### Scene Delegate (Non-Navigation)

Non-navigation apps receive an interface controller only. No window.

```swift
import CarPlay

final class CarPlaySceneDelegate: UIResponder,
    CPTemplateApplicationSceneDelegate {

    var interfaceController: CPInterfaceController?

    func templateApplicationScene(
        _ templateApplicationScene: CPTemplateApplicationScene,
        didConnect interfaceController: CPInterfaceController
    ) {
        self.interfaceController = interfaceController
        interfaceController.setRootTemplate(buildRootTemplate(),
                                            animated: true, completion: nil)
    }

    func templateApplicationScene(
        _ templateApplicationScene: CPTemplateApplicationScene,
        didDisconnectInterfaceController interfaceController: CPInterfaceController
    ) {
        self.interfaceController = nil
    }
}
```

### Scene Delegate (Navigation)

Navigation apps receive both an interface controller and a `CPWindow`.
Set the window's root view controller to draw map content.

```swift
func templateApplicationScene(
    _ templateApplicationScene: CPTemplateApplicationScene,
    didConnect interfaceController: CPInterfaceController,
    to window: CPWindow
) {
    self.interfaceController = interfaceController
    self.carWindow = window
    window.rootViewController = MapViewController()

    let mapTemplate = CPMapTemplate()
    mapTemplate.mapDelegate = self
    interfaceController.setRootTemplate(mapTemplate, animated: true,
                                        completion: nil)
}
```

## Templates Overview

CarPlay provides a fixed set of template types. The app supplies content;
the system renders it on the vehicle display.

### General Purpose Templates

| Template | Purpose |
|---|---|
| `CPTabBarTemplate` | Container with tabbed child templates |
| `CPListTemplate` | Scrollable sectioned list |
| `CPGridTemplate` | Grid of tappable icon buttons (max 8) |
| `CPInformationTemplate` | Key-value info with up to 3 actions |
| `CPAlertTemplate` | Modal alert with up to 2 actions |
| `CPActionSheetTemplate` | Modal action sheet |

### Category-Specific Templates

| Template | Category |
|---|---|
| `CPMapTemplate` | Navigation -- map overlay with nav bar |
| `CPSearchTemplate` | Navigation -- destination search |
| `CPNowPlayingTemplate` | Audio -- shared Now Playing screen |
| `CPPointOfInterestTemplate` | EV Charging / Parking / Food -- POI map |
| `CPContactTemplate` | Communication -- contact card |

### Navigation Hierarchy

Use `pushTemplate(_:animated:completion:)` to add templates to the stack.
Use `presentTemplate(_:animated:completion:)` for modal display.
Use `popTemplate(animated:completion:)` to go back.
`CPTabBarTemplate` must be set as root -- it cannot be pushed or presented.

### CPTabBarTemplate

```swift
let browseTab = CPListTemplate(title: "Browse",
                               sections: [CPListSection(items: listItems)])
browseTab.tabImage = UIImage(systemName: "list.bullet")

let tabBar = CPTabBarTemplate(templates: [browseTab, settingsTab])
tabBar.delegate = self
interfaceController.setRootTemplate(tabBar, animated: true, completion: nil)
```

### CPListTemplate

```swift
let item = CPListItem(text: "Favorites", detailText: "12 items")
item.handler = { selectedItem, completion in
    self.interfaceController?.pushTemplate(detailTemplate, animated: true,
                                           completion: nil)
    completion()
}

let section = CPListSection(items: [item], header: "Library",
                            sectionIndexTitle: nil)
let listTemplate = CPListTemplate(title: "My App", sections: [section])
```

## Navigation Apps

Navigation apps use `com.apple.developer.carplay-maps`. They are the only
category that receives a `CPWindow` for drawing map content. The root
template must be a `CPMapTemplate`.

### Trip Preview and Route Selection

```swift
let routeChoice = CPRouteChoice(
    summaryVariants: ["Fastest Route", "Fast"],
    additionalInformationVariants: ["Via Highway 101"],
    selectionSummaryVariants: ["25 min"]
)
let trip = CPTrip(origin: origin, destination: destination,
                  routeChoices: [routeChoice])
mapTemplate.showTripPreviews([trip], textConfiguration: nil)
```

### Starting a Navigation Session

```swift
extension CarPlaySceneDelegate: CPMapTemplateDelegate {
    func mapTemplate(_ mapTemplate: CPMapTemplate,
                     startedTrip trip: CPTrip,
                     using routeChoice: CPRouteChoice) {
        let session = mapTemplate.startNavigationSession(for: trip)
        session.pauseTrip(for: .loading, description: "Calculating route...")

        let maneuver = CPManeuver()
        maneuver.instructionVariants = ["Turn right onto Main St"]
        maneuver.symbolImage = UIImage(systemName: "arrow.turn.up.right")
        session.upcomingManeuvers = [maneuver]

        let estimates = CPTravelEstimates(
            distanceRemaining: Measurement(value: 5.2, unit: .miles),
            timeRemaining: 900)
        session.updateEstimates(estimates, for: maneuver)
    }
}
```

### Map Buttons

```swift
let zoomIn = CPMapButton { _ in self.mapViewController.zoomIn() }
zoomIn.image = UIImage(systemName: "plus.magnifyingglass")
mapTemplate.mapButtons = [zoomIn, zoomOut]
```

### CPSearchTemplate

```swift
extension CarPlaySceneDelegate: CPSearchTemplateDelegate {
    func searchTemplate(_ searchTemplate: CPSearchTemplate,
                        updatedSearchText searchText: String,
                        completionHandler: @escaping ([CPListItem]) -> Void) {
        performSearch(query: searchText) { results in
            completionHandler(results.map {
                CPListItem(text: $0.name, detailText: $0.address)
            })
        }
    }

    func searchTemplate(_ searchTemplate: CPSearchTemplate,
                        selectedResult item: CPListItem,
                        completionHandler: @escaping () -> Void) {
        // Navigate to selected destination
        completionHandler()
    }
}
```

## Audio Apps

Audio apps use `com.apple.developer.carplay-audio`. They display browsable
content in lists and use `CPNowPlayingTemplate` for playback controls.
`CPInformationTemplate` is not available to audio-entitled apps.

### Now Playing Template

`CPNowPlayingTemplate` is a shared singleton. It reads metadata from
`MPNowPlayingInfoCenter`. Do not instantiate a new one.

```swift
let nowPlaying = CPNowPlayingTemplate.shared
nowPlaying.isUpNextButtonEnabled = true
nowPlaying.isAlbumArtistButtonEnabled = true
nowPlaying.updateNowPlayingButtons([
    CPNowPlayingShuffleButton { _ in self.toggleShuffle() },
    CPNowPlayingRepeatButton { _ in self.toggleRepeat() }
])
nowPlaying.add(self) // Register as CPNowPlayingTemplateObserver
```

### Siri Assistant Cell

Audio apps supporting `INPlayMediaIntent` can show an assistant cell.
Communication apps use `INStartCallIntent` with `.startCall`.

```swift
let config = CPAssistantCellConfiguration(
    position: .top, visibility: .always, assistantAction: .playMedia)
let listTemplate = CPListTemplate(
    title: "Playlists",
    sections: [CPListSection(items: items)],
    assistantCellConfiguration: config)
```

## Communication Apps

Communication apps use `com.apple.developer.carplay-communication`.
They display message lists and contacts, and support `INStartCallIntent`
for Siri-initiated calls.
`CPMessageListItem` has no app-provided selection handler. When selected,
CarPlay invokes Siri compose, read, or reply behavior based on the item's
phone/email, unread state, or existing conversation configuration.

```swift
let leading = CPMessageListItemLeadingConfiguration(
    leadingItem: .star, leadingImage: nil, unread: true)
let trailing = CPMessageListItemTrailingConfiguration(
    trailingItem: .none, trailingImage: nil)

let message = CPMessageListItem(
    conversationIdentifier: "conv-123",
    text: "Jane",
    leadingConfiguration: leading,
    trailingConfiguration: trailing,
    detailText: "Meeting at 3pm",
    trailingText: "2:45 PM")

let messageList = CPListTemplate(title: "Messages",
                                 sections: [CPListSection(items: [message])])
```

## Point of Interest Apps

EV charging, parking, and food ordering apps use `CPPointOfInterestTemplate`
and `CPInformationTemplate` to display locations and details.
`CPPointOfInterestTemplate` displays a maximum of 12 points of interest.

### CPPointOfInterestTemplate

```swift
let poi = CPPointOfInterest(
    location: MKMapItem(placemark: MKPlacemark(
        coordinate: CLLocationCoordinate2D(latitude: 37.7749,
                                           longitude: -122.4194))),
    title: "SuperCharger Station", subtitle: "4 available",
    summary: "150 kW DC fast charging",
    detailTitle: "SuperCharger Station", detailSubtitle: "$0.28/kWh",
    detailSummary: "Open 24 hours",
    pinImage: UIImage(systemName: "bolt.fill"))

poi.primaryButton = CPTextButton(title: "Navigate",
                                 textStyle: .confirm) { _ in }

let poiTemplate = CPPointOfInterestTemplate(
    title: "Nearby Chargers", pointsOfInterest: [poi], selectedIndex: 0)
poiTemplate.pointOfInterestDelegate = self
```

### CPInformationTemplate

```swift
let infoTemplate = CPInformationTemplate(
    title: "Order Summary", layout: .leading,
    items: [
        CPInformationItem(title: "Item", detail: "Burrito Bowl"),
        CPInformationItem(title: "Total", detail: "$12.50")],
    actions: [
        CPTextButton(title: "Place Order", textStyle: .confirm) { _ in
            self.placeOrder() },
        CPTextButton(title: "Cancel", textStyle: .cancel) { _ in
            self.interfaceController?.popTemplate(animated: true,
                                                  completion: nil) }])
```

## Testing with CarPlay Simulator

1. Build and run in Xcode with the iOS simulator.
2. Choose I/O > External Displays > CarPlay.

Default window: 800x480 at `@2x`. Enable extra options for navigation apps:

```bash
defaults write com.apple.iphonesimulator CarPlayExtraOptions -bool YES
```

### Recommended Test Configurations

| Configuration | Pixels | Scale |
|---|---|---|
| Minimum | 748 x 456 | `@2x` |
| Portrait | 768 x 1024 | `@2x` |
| Standard | 800 x 480 | `@2x` |
| High-resolution | 1920 x 720 | @3x |

Simulator cannot test locked-iPhone behavior, Siri, audio coexistence with
car radio, or physical input hardware (knobs, touch pads). Test on a real
CarPlay-capable vehicle or aftermarket head unit when possible.
Design primary CarPlay flows so they do not require iPhone input while
CarPlay is active.

## Common Mistakes

### DON'T: Use the wrong scene delegate method

Navigation apps must implement `templateApplicationScene(_:didConnect:to:)`
(with `CPWindow`). Non-navigation apps use
`templateApplicationScene(_:didConnect:)` (no window). Using the wrong
variant produces no CarPlay UI.

### DON'T: Draw custom UI in the navigation window

`CPWindow` is exclusively for map content. All overlays, alerts, and
controls must use CarPlay templates.

### DON'T: Push or present CPTabBarTemplate

`CPTabBarTemplate` can only be set as root. Pushing or presenting it fails.
Use `setRootTemplate(_:animated:completion:)`.

### DON'T: Instantiate CPNowPlayingTemplate

Use `CPNowPlayingTemplate.shared`. Creating a new instance causes issues.

### DON'T: Add handlers to CPMessageListItem

`CPMessageListItem` is Siri-managed, unlike `CPListItem`. Do not set
`message.handler`; use the item configuration and `userInfo` for context.

### DON'T: Treat widgets as CarPlay template apps

CarPlay-visible widgets and Live Activities belong to WidgetKit and
ActivityKit. Use this skill for category-entitled CarPlay template app scenes
and for validating those surfaces in the car context.

### DON'T: Ignore vehicle display limits

Check `CPSessionConfiguration.limitedUserInterfaces` and respect
`maximumItemCount` / `maximumSectionCount` on list templates.

### DON'T: Forget to call the completion handler

`CPListItem.handler` must call its completion handler in every code path.
Failure leaves the list in a loading state.

## Review Checklist

- [ ] Correct CarPlay entitlement key in `Entitlements.plist`
- [ ] `UIApplicationSupportsMultipleScenes` set to `true`
- [ ] `CPTemplateApplicationSceneSessionRoleApplication` scene in Info.plist
- [ ] Scene delegate class name matches `UISceneDelegateClassName`
- [ ] Correct delegate method used (with/without `CPWindow`)
- [ ] Root template set in `didConnect` before returning
- [ ] Interface controller and window references cleared on disconnect
- [ ] `CPTabBarTemplate` only used as root, never pushed
- [ ] `CPNowPlayingTemplate.shared` used, not a new instance
- [ ] Communication rows use `CPMessageListItem` without custom handlers
- [ ] WidgetKit/ActivityKit surfaces routed outside CarPlay template app code
- [ ] `maximumItemCount`/`maximumSectionCount` checked before populating lists
- [ ] `CPListItem.handler` calls completion in every path
- [ ] Map-only content in `CPWindow` root view controller (navigation apps)
- [ ] App functions while iPhone is locked
- [ ] Tested at minimum, standard, and high-resolution simulator sizes
- [ ] Audio session deactivated when not actively playing

## References

- Extended patterns (dashboard, instrument cluster, full nav flow, tab composition): [references/carplay-patterns.md](references/carplay-patterns.md)
- [CarPlay framework](https://sosumi.ai/documentation/carplay)
- [CPTemplateApplicationSceneDelegate](https://sosumi.ai/documentation/carplay/cptemplateapplicationscenedelegate)
- [CPInterfaceController](https://sosumi.ai/documentation/carplay/cpinterfacecontroller)
- [CPMapTemplate](https://sosumi.ai/documentation/carplay/cpmaptemplate)
- [CPListTemplate](https://sosumi.ai/documentation/carplay/cplisttemplate)
- [CPNowPlayingTemplate](https://sosumi.ai/documentation/carplay/cpnowplayingtemplate)
- [CPPointOfInterestTemplate](https://sosumi.ai/documentation/carplay/cppointofinteresttemplate)
- [CPNavigationSession](https://sosumi.ai/documentation/carplay/cpnavigationsession)
- [Requesting CarPlay Entitlements](https://sosumi.ai/documentation/carplay/requesting-carplay-entitlements)
- [Displaying Content in CarPlay](https://sosumi.ai/documentation/carplay/displaying-content-in-carplay)
- [Using the CarPlay Simulator](https://sosumi.ai/documentation/carplay/using-the-carplay-simulator)
- [CarPlay HIG](https://sosumi.ai/design/human-interface-guidelines/carplay)

More from dpearson2699/swift-ios-skills

SkillDescription
accessorysetupkitDiscover and configure Bluetooth and Wi-Fi accessories using AccessorySetupKit. Use when presenting a privacy-preserving accessory picker, defining discovery descriptors for BLE or Wi-Fi devices, handling accessory session events, migrating from CoreBluetooth permission-based scanning, or setting up accessories without requiring broad Bluetooth permissions.
activitykitImplement, review, or improve Live Activities and Dynamic Island experiences in iOS apps using ActivityKit. Use when building real-time updating widgets for the Lock Screen and Dynamic Island — delivery tracking, sports scores, ride-sharing status, workout timers, media playback, or any time-sensitive information that updates in real time. Also use when working with ActivityKit, ActivityAttributes, Activity lifecycle (request/update/end), Dynamic Island layouts (compact/minimal/expanded), push-to-update Live Activities, or Lock Screen live widgets.
adattributionkitMeasure ad effectiveness with privacy-preserving attribution using AdAttributionKit. Use when registering ad impressions, handling attribution postbacks, updating conversion values, implementing re-engagement attribution, configuring publisher or advertiser apps, or replacing SKAdNetwork with AdAttributionKit for ad measurement.
alarmkitImplement AlarmKit alarms and countdown timers for iOS and iPadOS with Lock Screen, Dynamic Island, StandBy, and paired Apple Watch system UI. Covers AlarmManager scheduling, AlarmAttributes and AlarmPresentation, system Stop and AlarmButton secondary actions, authorization, state observation, countdown widget-extension handoff, and Live Activity integration. Use when building wake-up alarms, countdown timers, or alarm-style alerts that need Apple's system alarm experience.
app-clipsBuild iOS App Clips with invocation URLs, App Clip Codes, NFC, QR codes, Safari banners, Maps, Messages, target setup, App Store Connect experiences, size/capability constraints, NSUserActivity routing, SKOverlay promotion, App Group/keychain handoff, ephemeral notifications, location confirmation, and full-app migration. Use when creating App Clips or wiring App Clip invocation, experience configuration, or full-app handoff.
app-intentsImplement App Intents for Siri, Shortcuts, Spotlight, widgets, Control Center, and Apple Intelligence on iOS. Covers AppIntent actions, AppEntity and EntityQuery models, AppShortcutsProvider phrases, IndexedEntity Spotlight indexing, WidgetConfigurationIntent, SnippetIntent, and assistant schemas. Use when exposing app actions or entities to system surfaces.
app-store-reviewAudits App Store submission readiness and rejection risk across current review guidelines, PrivacyInfo.xcprivacy and required-reason APIs, privacy labels, ATT, StoreKit payments, metadata, entitlements, widgets, and Live Activities. Use when preparing a submission, responding to rejection, reconciling privacy evidence, or separating upload blockers from cleanup.
apple-on-device-aiBuild private, on-device AI features on iPhone, iPad, and Mac with Foundation Models, Core ML, MLX Swift, or llama.cpp. Use when choosing an Apple-local model runtime, building an Apple Intelligence chatbot or tool-calling feature, running an LLM on Apple Silicon, converting or compressing a Python model for Core ML, or comparing on-device inference backends. For Swift Core ML loading and prediction code, use the coreml skill.
appmigrationkitTransfer app data to or from other platforms using AppMigrationKit. Use when implementing system-orchestrated one-time migration between iOS and Android or another platform, building an AppMigrationExtension, packaging transportable resources with ResourcesArchiver, importing resources on the destination device, reporting import progress, handling migration errors and app group cleanup, checking MigrationStatus, or testing migration code with AppMigrationTester.
audioaccessorykitSupport automatic audio switching for paired third-party Bluetooth headphones or earbuds with AudioAccessoryKit. Use when a companion app registers an audio accessory, an app extension reports worn/removed placement or connected source-device changes, or AccessoryControlDevice capabilities and errors need handling. Do not use for general AVAudioSession routing, Bluetooth transport, or initial accessory pairing.