| name | carplay |
| description | CarPlay development patterns including CPTemplateApplicationScene, navigation, audio, communication, and EV charging templates. Use when building or adapting apps for CarPlay. |
First step: Tell the user: "carplay skill loaded."
CarPlay Development
Guidance for building CarPlay-enabled apps using the CarPlay framework, including template selection, scene lifecycle, navigation, audio, communication, and testing.
When This Skill Activates
Use this skill when the user:
- Asks about CarPlay development or integration
- Wants to build a navigation, audio, communication, or EV charging app for CarPlay
- Needs help choosing CarPlay templates (CPListTemplate, CPMapTemplate, etc.)
- Asks about CPTemplateApplicationScene or multi-scene lifecycle
- Wants to support instrument cluster / multi-screen display
- Needs to handle Siri intents within CarPlay
- Asks about CarPlay Simulator testing
- Wants to understand CarPlay entitlement and provisioning requirements
- Is adapting an existing iPhone app for the car
Decision Tree: Choosing Your CarPlay App Type
What does your app do?
├── Turn-by-turn navigation ──────────► Navigation app (CPMapTemplate)
├── Stream or play audio ─────────────► Audio app (CPNowPlayingTemplate)
├── Messaging / VoIP calls ───────────► Communication app (CPMessageListItem, CPContact)
├── EV charging station finder ───────► EV Charging app (CPPointOfInterestTemplate)
├── Quick food ordering ──────────────► Quick Food Ordering app (CPListTemplate)
├── Fueling station finder ───────────► Fueling app (CPPointOfInterestTemplate)
├── Parking garage finder ────────────► Parking app (CPPointOfInterestTemplate)
└── Driving task (toll, road condition)► Driving Task app (CPListTemplate)
Each category requires a specific CarPlay entitlement from Apple. You cannot mix categories.
API Availability
| API / Feature | Minimum OS | Notes |
|---|
| CarPlay framework (templates) | iOS 12 | Replaced UIScreen-based approach |
| CPTemplateApplicationScene | iOS 13 | Scene-based lifecycle (required) |
| CPTemplateApplicationDashboardScene | iOS 13.4 | Instrument cluster support |
| CPNowPlayingTemplate | iOS 14 | Standalone now-playing screen |
| CPPointOfInterestTemplate | iOS 14 | POI display with map pins |
| CPInformationTemplate | iOS 14 | Key-value detail screens |
| CPTabBarTemplate (5 tabs max) | iOS 14 | Tab-based root navigation |
| CPListTemplate sectioned headers | iOS 15 | Section headers and filtering |
| Instrument cluster map | iOS 15.4 | Navigation apps only |
Entitlement and Provisioning Setup
- Request the CarPlay entitlement from Apple at developer.apple.com/carplay.
- Apple grants a category-specific entitlement (e.g.,
com.apple.developer.carplay-audio).
- Add the entitlement to your provisioning profile and
Entitlements.plist:
- Declare the CPTemplateApplicationScene in
Info.plist:
<key>UIApplicationSceneManifest</key>
<dict>
<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).CarPlaySceneDelegate</string>
</dict>
</array>
</dict>
</dict>
CPTemplateApplicationScene Lifecycle
import CarPlay
class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate {
var interfaceController: CPInterfaceController?
func templateApplicationScene(
_ templateApplicationScene: CPTemplateApplicationScene,
didConnect interfaceController: CPInterfaceController
) {
self.interfaceController = interfaceController
let rootTemplate = buildRootTemplate()
interfaceController.setRootTemplate(rootTemplate, animated: true, completion: nil)
}
func templateApplicationScene(
_ templateApplicationScene: CPTemplateApplicationScene,
didDisconnectInterfaceController interfaceController: CPInterfaceController
) {
self.interfaceController = nil
}
}
Dashboard Scene (Instrument Cluster)
class CarPlayDashboardDelegate: UIResponder, CPTemplateApplicationDashboardSceneDelegate {
func templateApplicationDashboardScene(
_ templateApplicationDashboardScene: CPTemplateApplicationDashboardScene,
didConnect dashboardController: CPDashboardController
) {
let button = CPDashboardButton(
titleVariants: ["Navigate"],
subtitleVariants: ["To destination"],
image: UIImage(systemName: "location.fill")!
) { _ in
}
dashboardController.shortcutButtons = [button]
}
}
Template Types
CPTabBarTemplate (Root Container)
func buildRootTemplate() -> CPTabBarTemplate {
let favoritesSection = CPListItem(text: "Home", detailText: "123 Main St")
let favoritesTemplate = CPListTemplate(
title: "Favorites",
sections: [CPListSection(items: [favoritesSection])]
)
favoritesTemplate.tabImage = UIImage(systemName: "star.fill")
let recentsTemplate = CPListTemplate(
title: "Recents",
sections: []
)
recentsTemplate.tabImage = UIImage(systemName: "clock.fill")
return CPTabBarTemplate(templates: [favoritesTemplate, recentsTemplate])
}
CPListTemplate
let item = CPListItem(text: "Coffee Shop", detailText: "0.3 mi away")
item.handler = { listItem, completion in
completion()
}
let section = CPListSection(items: [item], header: "Nearby", sectionIndexTitle: "N")
let listTemplate = CPListTemplate(title: "Places", sections: [section])
CPGridTemplate
let gridButton = CPGridButton(
titleVariants: ["Gas"],
image: UIImage(systemName: "fuelpump.fill")!
) { _ in
}
let gridTemplate = CPGridTemplate(title: "Categories", gridButtons: [gridButton])
CPInformationTemplate and CPPointOfInterestTemplate
let infoItem = CPInformationItem(title: "Distance", detail: "2.4 mi")
let infoTemplate = CPInformationTemplate(
title: "Station Details",
layout: .leading,
items: [infoItem],
actions: [CPTextButton(title: "Navigate", textStyle: .confirm) { _ in }]
)
let poi = CPPointOfInterest(
location: MKMapItem(placemark: MKPlacemark(coordinate: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194))),
title: "Charger A",
subtitle: "Available",
summary: "50 kW DC Fast",
detailTitle: "Charger A",
detailSubtitle: "50 kW DC Fast Charger",
detailSummary: "Available now — 2 of 4 ports open",
pinImage: UIImage(systemName: "bolt.fill")
)
let poiTemplate = CPPointOfInterestTemplate(title: "Chargers", pointsOfInterest: [poi], selectedIndex: 0)
CPAlertTemplate and CPActionSheetTemplate
let alert = CPAlertTemplate(
titleVariants: ["Reroute?"],
actions: [
CPAlertAction(title: "Yes", style: .default) { _ in },
CPAlertAction(title: "No", style: .cancel) { _ in }
]
)
let actionSheet = CPActionSheetTemplate(
title: "Options",
message: "Choose an action",
actions: [
CPAlertAction(title: "Share ETA", style: .default) { _ in },
CPAlertAction(title: "Cancel", style: .cancel) { _ in }
]
)
Navigation Apps
class NavigationManager: CPMapTemplateDelegate {
func startNavigation(on mapTemplate: CPMapTemplate) {
let trip = CPTrip(
origin: MKMapItem.forCurrentLocation(),
destination: destinationMapItem,
routeChoices: [
CPRouteChoice(
summaryVariants: ["Fastest — 25 min"],
additionalInformationVariants: ["via Highway 101"],
selectionSummaryVariants: ["25 min via 101"]
)
]
)
mapTemplate.showTripPreviews([trip])
let session = mapTemplate.startNavigationSession(for: trip)
session.pauseTrip(for: .loading, description: "Calculating…")
let maneuver = CPManeuver()
maneuver.instructionVariants = ["Turn right onto Oak St"]
maneuver.symbolImage = UIImage(systemName: "arrow.turn.up.right")
session.upcomingManeuvers = [maneuver]
session.resumeTrip(with: .routeGuidance, description: "Navigating")
}
func mapTemplate(_ mapTemplate: CPMapTemplate, panWith direction: CPMapTemplate.PanDirection) {
}
( : ) {
}
}
Instrument Cluster Map (iOS 15.4+)
func templateApplicationScene(
_ scene: CPTemplateApplicationScene,
didConnect interfaceController: CPInterfaceController,
to window: CPWindow
) {
let clusterVC = ClusterMapViewController()
window.rootViewController = clusterVC
}
Audio Apps
class AudioCarPlayManager: CPTemplateApplicationSceneDelegate {
func templateApplicationScene(
_ scene: CPTemplateApplicationScene,
didConnect interfaceController: CPInterfaceController
) {
let playlists = CPListItem(text: "My Playlists", detailText: "12 playlists")
playlists.handler = { _, completion in
completion()
}
playlists.accessoryType = .disclosureIndicator
let browseTemplate = CPListTemplate(
title: "Music",
sections: [CPListSection(items: [playlists])]
)
browseTemplate.tabImage = UIImage(systemName: "music.note.list")
let nowPlaying = CPNowPlayingTemplate.shared
nowPlaying.updateNowPlayingButtons([
CPNowPlayingShuffleButton { _ in },
CPNowPlayingRepeatButton { _ in }
])
let tabBar = CPTabBarTemplate(templates: [browseTemplate, nowPlaying])
interfaceController.setRootTemplate(tabBar, animated: true, completion: )
}
}
Audio apps must use MPNowPlayingInfoCenter and MPRemoteCommandCenter for playback metadata and controls.
Communication Apps
let message = CPMessageListItem(
conversationIdentifier: "conv-123",
text: "Alice",
leadingConfiguration: CPMessageListItem.LeadingConfiguration(
leadingItem: .init(text: "A", textStyle: .abbreviation),
unread: true
),
trailingConfiguration: nil,
trailingText: "2m ago"
)
let contact = CPContact(name: PersonNameComponents(givenName: "Alice", familyName: "Smith"))
contact.actions = [
CPButton(
image: UIImage(systemName: "phone.fill")!,
handler: { _ in }
)
]
Communication apps use INSendMessageIntent and INStartCallIntent for Siri integration.
Handling Siri and CarPlay Voice Commands
class IntentHandler: INExtension, INStartAudioMediaIntentHandling {
func handle(intent: INStartAudioMediaIntent) async -> INStartAudioMediaIntentResponse {
guard let mediaSearch = intent.mediaSearch else {
return INStartAudioMediaIntentResponse(code: .failure, userActivity: nil)
}
return INStartAudioMediaIntentResponse(code: .success, userActivity: nil)
}
}
Declare supported intents in Info.plist under INIntentsSupported.
Testing in CarPlay Simulator
- Xcode CarPlay Simulator: Window > Devices and Simulators > select your simulator > enable CarPlay from Features menu.
- External display simulation: Use
Additional Simulator in Xcode to test instrument cluster.
- Hardware testing: Use a CarPlay-capable head unit or Apple's MFi CarPlay development kit.
Key testing checklist:
- Verify scene connect/disconnect lifecycle
- Test template stack depth (max 5 pushed templates)
- Validate all list items call their
completion() handler
- Test with Siri ("Hey Siri, play X on MyApp")
- Test interruptions (phone calls, other audio sources)
Common Pitfalls
| Pitfall | Detail |
|---|
| Template stack limit | Maximum 5 templates on the navigation stack. Plan your hierarchy. |
| No custom UI | You cannot draw custom UIKit views on the CarPlay screen (except map content in navigation apps). |
| Entitlement approval | Apple reviews CarPlay entitlement requests manually — apply early. |
| Forgetting completion handlers | Every CPListItem.handler must call completion() or the UI freezes. |
| Missing scene config | If Info.plist is missing the scene role, CarPlay silently never connects. |
| Audio session category | Audio apps must set .playback category with .mixWithOthers or .duckOthers policy. |
Patterns
✅ Good Patterns
item.handler = { item, completion in
self.showDetail(for: item)
completion()
}
let maneuver = CPManeuver()
maneuver.instructionVariants = [
"Turn right onto Oak Street",
"Turn right — Oak St",
"Right on Oak"
]
if (interfaceController.templates.count < 5) {
interfaceController.pushTemplate(detailTemplate, animated: true, completion: nil)
}
func templateApplicationScene(_ scene: CPTemplateApplicationScene,
didDisconnectInterfaceController controller: CPInterfaceController) {
self.interfaceController = nil
self.navigationSession = nil
}
❌ Bad Patterns
item.handler = { item, completion in
self.showDetail(for: item)
}
interfaceController.pushTemplate(myCustomViewController, animated: true)
for category in allCategories {
interfaceController.pushTemplate(category.template, animated: true, completion: nil)
}
maneuver.instructionVariants = ["Turn right onto Oak Street and merge onto Highway 101 North"]