Loading...
Loading...
Guide for building CarPlay quick-ordering apps (food ordering, pickup, etc.) using the CarPlay framework. Use this skill whenever the user wants to build a CarPlay ordering app, integrate CarPlay templates for food/drink/pickup ordering, work with CPPointOfInterestTemplate, CPListTemplate, CPTabBarTemplate, or CPInterfaceController for ordering workflows. Also trigger when the user mentions CarPlay entitlements, Live Activities with CarPlay ordering, or push notification updates for order status. Covers the full lifecycle: entitlement setup, template hierarchy, map-based POI selection, order placement, Live Activity integration, and push token management.
npx skill4agent add ios-agent/iosagent.dev carplay-orderingCPPointOfInterestTemplateCPListTemplateCPTabBarTemplateEntitlements.plistCODE_SIGN_ENTITLEMENTSEntitlements.plistCPTemplateApplicationSceneDelegateCPInterfaceController (root controller)
└── CPTabBarTemplate
└── CPPointOfInterestTemplate (map with ordering locations)
└── CPListTemplate (order details/menu items)CPInterfaceControllerCPTabBarTemplateCPPointOfInterestTemplateCPListTemplateCPTextButtonCPAlertTemplateCPSessionConfigurationCPTemplateApplicationSceneDelegatefunc interfaceControllerDidConnect(
_ interfaceController: CPInterfaceController,
scene: CPTemplateApplicationScene
) {
carplayInterfaceController = interfaceController
carplayScene = scene
carplayInterfaceController?.delegate = self
sessionConfiguration = CPSessionConfiguration(delegate: self)
locationManager.delegate = self
requestLocation()
setupMap()
}CPTabBarTemplateCPPointOfInterestTemplatefunc setupMap() {
let poiTemplate = CPPointOfInterestTemplate(
title: "Options",
pointsOfInterest: [],
selectedIndex: NSNotFound
)
poiTemplate.pointOfInterestDelegate = self
poiTemplate.tabTitle = "Map"
poiTemplate.tabImage = UIImage(systemName: "car")!
let tabTemplate = CPTabBarTemplate(templates: [poiTemplate])
carplayInterfaceController?.setRootTemplate(tabTemplate, animated: true) { done, error in
self.search(for: "YourSearchTerm")
}
}Important: A maximum of 12 POI locations can appear on the CarPlay display.
CPPointOfInterestTemplateDelegateextension TemplateManager: CPPointOfInterestTemplateDelegate {
func pointOfInterestTemplate(
_ aTemplate: CPPointOfInterestTemplate,
didChangeMapRegion region: MKCoordinateRegion
) {
boundingRegion = region
search(for: "yourQuery")
}
}// Primary: Order button
let orderButton = CPTextButton(title: "Order", textStyle: .normal) { button in
self.showOrderTemplate(place: place)
}
place.primaryButton = orderButton
// Secondary: Directions (via Maps) or Call
if let address = place.summary,
let encoded = address.addingPercentEncoding(withAllowedCharacters: .alphanumerics),
let lon = place.location.placemark.location?.coordinate.longitude,
let lat = place.location.placemark.location?.coordinate.latitude,
let url = URL(string: "maps://?q=\(encoded)&ll=\(lon),\(lat)") {
place.secondaryButton = CPTextButton(title: "Directions", textStyle: .normal) { _ in
self.carplayScene?.open(url, options: nil, completionHandler: nil)
}
} else if let phone = place.subtitle,
let url = URL(string: "tel://" + phone.replacingOccurrences(of: " ", with: "")) {
place.secondaryButton = CPTextButton(title: "Call", textStyle: .normal) { _ in
self.carplayScene?.open(url, options: nil, completionHandler: nil)
}
}func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
switch manager.authorizationStatus {
case .denied, .restricted, .notDetermined:
let alert = CPAlertTemplate(
titleVariants: ["Please enable location services."],
actions: [
CPAlertAction(title: "Ok", style: .default) { [weak self] _ in
self?.carplayInterfaceController?.setRootTemplate(
CPTabBarTemplate(templates: []),
animated: false,
completion: nil
)
}
]
)
// Dismiss any existing presented template first
if carplayInterfaceController?.presentedTemplate != nil {
dismissAlertAndPopToRootTemplate {
self.carplayInterfaceController?.presentTemplate(alert, animated: false, completion: nil)
}
} else {
carplayInterfaceController?.presentTemplate(alert, animated: false, completion: nil)
}
default:
dismissAlertAndPopToRootTemplate {
self.setupMap()
}
}
}let attrs = OrderStatusAttributes(order: order)
let initialState = OrderStatusAttributes.ContentState(
isPickedUp: false,
isReady: false,
isPreparing: false,
isConfirmed: true
)
let activity = try Activity.request(
attributes: attrs,
content: .init(state: initialState, staleDate: Date(timeIntervalSinceNow: 60 * 30)),
pushType: .token
)// Content updates
Task { @MainActor in
for await change in activity.contentUpdates {
try saveOrderState(state: change.state)
WidgetCenter.shared.reloadAllTimelines()
}
}
// Activity state (ended/dismissed)
Task { @MainActor in
for await state in activity.activityStateUpdates {
if state == .dismissed || state == .ended {
await activity.end(nil, dismissalPolicy: .immediate)
}
WidgetCenter.shared.reloadAllTimelines()
}
}
// Push token for remote updates
Task { @MainActor in
for await pushToken in activity.pushTokenUpdates {
let tokenString = pushToken.reduce("") { $0 + String(format: "%02x", $1) }
try await sendPushToken(order: order, pushTokenString: tokenString)
}
}let privateKey = try P256.Signing.PrivateKey(pemRepresentation: pemString)
let header = try JSONEncoder().encode(header).urlSafeBase64EncodedString()
let payload = try JSONEncoder().encode(payload).urlSafeBase64EncodedString()
let toSign = Data((header + "." + payload).utf8)
let signature = try privateKey.signature(for: toSign)
let token = [header, payload, signature.rawRepresentation.urlSafeBase64EncodedString()]
.joined(separator: ".")CPPointOfInterestTemplateCPInformationTemplateCPTextButton