From apple-kit-skills
Build CarPlay apps in Swift for iOS vehicle displays using templates (CPMapTemplate, CPNowPlayingTemplate), entitlements, scene lifecycle, and Simulator testing for navigation, audio, messaging, EV, parking, food apps.
npx claudepluginhub dpearson2699/swift-ios-skills --plugin all-ios-skillsThis skill uses the workspace's default tool permissions.
Build apps that display on the vehicle's CarPlay screen using the CarPlay
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
Build apps that display on the vehicle's CarPlay screen using the CarPlay framework's template-based UI system. Covers scene lifecycle, template types, navigation guidance, audio playback, communication, point-of-interest categories, entitlement setup, and simulator testing. Targets Swift 6.3 / iOS 26+.
See references/carplay-patterns.md for extended patterns including full navigation sessions, dashboard scenes, and advanced template composition.
CarPlay requires a category-specific entitlement granted by Apple. Request it at developer.apple.com/contact/carplay and agree to the CarPlay Entitlement Addendum.
| 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 | Manages the template navigation hierarchy |
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.
<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.
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
}
}
Navigation apps receive both an interface controller and a CPWindow.
Set the window's root view controller to draw map content.
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)
}
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.
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)
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 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.
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)
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)
}
}
let zoomIn = CPMapButton { _ in self.mapViewController.zoomIn() }
zoomIn.image = UIImage(systemName: "plus.magnifyingglass")
mapTemplate.mapButtons = [zoomIn, zoomOut]
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 use com.apple.developer.carplay-audio. They display browsable
content in lists and use CPNowPlayingTemplate for playback controls.
CPNowPlayingTemplate is a shared singleton. It reads metadata from
MPNowPlayingInfoCenter. Do not instantiate a new one.
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
Audio apps supporting INPlayMediaIntent can show an assistant cell.
Communication apps use INStartCallIntent with .startCall.
let 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.
let message = CPMessageListItem(
conversationIdentifier: "conv-123",
text: "Meeting at 3pm",
leadingConfiguration: CPMessageListItem.LeadingConfiguration(
leadingItem: .init(text: "Jane", textStyle: .abbreviated),
unread: true),
trailingConfiguration: CPMessageListItem.TrailingConfiguration(
trailingItem: .init(text: "2:45 PM")),
trailingText: nil, trailingImage: nil)
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.
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
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) }])
Default window: 800x480 at @2x. Enable extra options for navigation apps:
defaults 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.
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.
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 instancemaximumItemCount/maximumSectionCount checked before populating listsCPListItem.handler calls completion in every pathCPWindow root view controller (navigation apps)