Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-20 | ✗→✓ | ▲ Improved | 147% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 118% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 141% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 122% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 131% | 0% |
Build category-entitled, template-based CarPlay apps for the vehicle display. Scope: Swift 6.3, iOS 26+.
See 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.
Request the category-specific entitlement from Apple's CarPlay entitlement form, accept the addendum, then provision the approved category key below.
| 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 |
Entitlements.plist with the entitlement key set to true.Entitlements.plist path.| 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 |
Declare the CarPlay scene in Info.plist and implement CPTemplateApplicationSceneDelegate to respond when CarPlay connects.
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>
Non-navigation apps receive an interface controller only. No window.
swiftimport 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 } }
Navigation apps receive both an interface controller and a CPWindow. Set the window's root view controller to draw map content.
swiftfunc 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) }
CarPlay provides a fixed set of template types. The app supplies content; the system renders it on the vehicle display.
| 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 |
| 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 |
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.
swiftlet 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)
swiftlet 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 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.
swiftlet 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)
swiftextension 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) } }
swiftlet zoomIn = CPMapButton { _ in self.mapViewController.zoomIn() } zoomIn.image = UIImage(systemName: "plus.magnifyingglass") mapTemplate.mapButtons = [zoomIn, zoomOut]
swiftextension 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 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.
CPNowPlayingTemplate is a shared singleton. It reads metadata from MPNowPlayingInfoCenter. Do not instantiate a new one.
swiftlet 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
Audio apps supporting INPlayMediaIntent can show an assistant cell. Communication apps use INStartCallIntent with .startCall.
swiftlet config = CPAssistantCellConfiguration( position: .top, visibility: .always, assistantAction: .playMedia) let listTemplate = CPListTemplate( title: "Playlists", sections: [CPListSection(items: items)], assistantCellConfiguration: config)
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.
swiftlet 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])])
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.
swiftlet 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
swiftlet 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) }])
Default window: 800x480 at @2x. Enable extra options for navigation apps:
bashdefaults write com.apple.iphonesimulator CarPlayExtraOptions -bool YES
| 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.
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.
CPWindow is exclusively for map content. All overlays, alerts, and controls must use CarPlay templates.
CPTabBarTemplate can only be set as root. Pushing or presenting it fails. Use setRootTemplate(_:animated:completion:).
Use CPNowPlayingTemplate.shared. Creating a new instance causes issues.
CPMessageListItem is Siri-managed, unlike CPListItem. Do not set message.handler; use the item configuration and userInfo for context.
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.
Check CPSessionConfiguration.limitedUserInterfaces and respect maximumItemCount / maximumSectionCount on list templates.
CPListItem.handler must call its completion handler in every code path. Failure leaves the list in a loading state.
Entitlements.plistUIApplicationSupportsMultipleScenes set to trueCPTemplateApplicationSceneSessionRoleApplication scene in Info.plistUISceneDelegateClassNameCPWindow)didConnect before returningCPTabBarTemplate only used as root, never pushedCPNowPlayingTemplate.shared used, not a new instanceCPMessageListItem without custom handlersmaximumItemCount/maximumSectionCount checked before populating listsCPListItem.handler calls completion in every pathCPWindow root view controller (navigation apps)Other measured skills in the registry, with their headline benchmark lift.