carplay-ordering
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCarPlay Quick-Ordering App Integration
CarPlay快速点餐应用集成
Build a CarPlay-enabled ordering app that displays custom ordering options in a vehicle using Apple's CarPlay framework.
使用苹果的CarPlay框架构建支持CarPlay的点餐应用,可在车载系统中展示自定义点餐选项。
When to Use This Skill
适用场景
- Building a food/drink ordering app with CarPlay support
- Integrating ,
CPPointOfInterestTemplate, orCPListTemplateCPTabBarTemplate - Setting up CarPlay entitlements and provisioning profiles
- Implementing order status with Live Activities from a CarPlay context
- Handling map region changes and location-based search in CarPlay
- Managing push notifications for order status updates
- 构建支持CarPlay的餐饮/饮品点餐应用
- 集成、
CPPointOfInterestTemplate或CPListTemplateCPTabBarTemplate - 配置CarPlay权限和配置文件
- 在CarPlay场景下通过Live Activities实现订单状态展示
- 处理CarPlay中的地图区域变更和基于位置的搜索
- 管理订单状态更新的推送通知
Prerequisites
前置条件
- iOS 17.2+ / iPadOS 17.2+ / Mac Catalyst 17.2+ / macOS 14.0+
- Xcode 15.4+
- CarPlay quick-ordering entitlement (request at https://developer.apple.com/contact/carplay)
- iOS 17.2+ / iPadOS 17.2+ / Mac Catalyst 17.2+ / macOS 14.0+
- Xcode 15.4+
- CarPlay快速点餐权限(申请地址:https://developer.apple.com/contact/carplay)
Entitlement Setup
权限配置
- Log in to Apple Developer and create a provisioning profile with the CarPlay quick-ordering entitlement
- Import the provisioning profile into Xcode
- Create an (if not already present)
Entitlements.plist - Add the CarPlay quick-ordering entitlement key as a Boolean
- Ensure in target build settings points to the
CODE_SIGN_ENTITLEMENTSEntitlements.plist
- 登录苹果开发者平台,创建包含CarPlay快速点餐权限的配置文件
- 将配置文件导入Xcode
- 若尚未创建,则新建该文件
Entitlements.plist - 添加CarPlay快速点餐权限键并设置为布尔类型
- 确保目标构建设置中的指向
CODE_SIGN_ENTITLEMENTSEntitlements.plist
Architecture Overview
架构概述
The app connects to CarPlay via . The template hierarchy is:
CPTemplateApplicationSceneDelegateCPInterfaceController (root controller)
└── CPTabBarTemplate
└── CPPointOfInterestTemplate (map with ordering locations)
└── CPListTemplate (order details/menu items)Key classes and their roles:
- — Manages the template stack and presentation
CPInterfaceController - — Top-level tab container
CPTabBarTemplate - — Map view showing up to 12 POI locations
CPPointOfInterestTemplate - — Displays menu items and order options
CPListTemplate - — Action buttons (Order, Directions, Call)
CPTextButton - — Alert dialogs (e.g., location permission prompts)
CPAlertTemplate - — Session configuration delegate
CPSessionConfiguration
应用通过与CarPlay建立连接。模板层级结构如下:
CPTemplateApplicationSceneDelegateCPInterfaceController (根控制器)
└── CPTabBarTemplate
└── CPPointOfInterestTemplate(带点餐地点的地图)
└── CPListTemplate(订单详情/菜单选项)核心类及其作用:
- — 管理模板栈和展示逻辑
CPInterfaceController - — 顶层标签容器
CPTabBarTemplate - — 展示最多12个POI位置的地图视图
CPPointOfInterestTemplate - — 展示菜单选项和订单内容
CPListTemplate - — 操作按钮(如“下单”“导航”“致电”)
CPTextButton - — 提示对话框(如位置权限申请提示)
CPAlertTemplate - — 会话配置代理
CPSessionConfiguration
Connection Lifecycle
连接生命周期
When CarPlay connects, implement :
CPTemplateApplicationSceneDelegateswift
func interfaceControllerDidConnect(
_ interfaceController: CPInterfaceController,
scene: CPTemplateApplicationScene
) {
carplayInterfaceController = interfaceController
carplayScene = scene
carplayInterfaceController?.delegate = self
sessionConfiguration = CPSessionConfiguration(delegate: self)
locationManager.delegate = self
requestLocation()
setupMap()
}Set the root template as a containing a :
CPTabBarTemplateCPPointOfInterestTemplateswift
func 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.
当CarPlay连接时,实现:
CPTemplateApplicationSceneDelegateswift
func interfaceControllerDidConnect(
_ interfaceController: CPInterfaceController,
scene: CPTemplateApplicationScene
) {
carplayInterfaceController = interfaceController
carplayScene = scene
carplayInterfaceController?.delegate = self
sessionConfiguration = CPSessionConfiguration(delegate: self)
locationManager.delegate = self
requestLocation()
setupMap()
}将包含的设置为根模板:
CPPointOfInterestTemplateCPTabBarTemplateswift
func 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")
}
}重要提示: CarPlay屏幕最多可显示12个POI位置。
Map Region Updates
地图区域更新
Implement to refresh results as the user pans the map:
CPPointOfInterestTemplateDelegateswift
extension TemplateManager: CPPointOfInterestTemplateDelegate {
func pointOfInterestTemplate(
_ aTemplate: CPPointOfInterestTemplate,
didChangeMapRegion region: MKCoordinateRegion
) {
boundingRegion = region
search(for: "yourQuery")
}
}实现,在用户拖动地图时刷新搜索结果:
CPPointOfInterestTemplateDelegateswift
extension TemplateManager: CPPointOfInterestTemplateDelegate {
func pointOfInterestTemplate(
_ aTemplate: CPPointOfInterestTemplate,
didChangeMapRegion region: MKCoordinateRegion
) {
boundingRegion = region
search(for: "yourQuery")
}
}POI Action Buttons
POI操作按钮
Each point of interest supports a primary and secondary button. Use the primary for ordering, and the secondary for navigation or calling:
swift
// 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)
}
}每个兴趣点支持一个主按钮和一个次按钮。主按钮用于下单,次按钮可用于导航或致电:
swift
// 主按钮:下单
let orderButton = CPTextButton(title: "Order", textStyle: .normal) { button in
self.showOrderTemplate(place: place)
}
place.primaryButton = orderButton
// 次按钮:导航(通过地图应用)或致电
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)
}
}Location Permission Handling
位置权限处理
Handle authorization changes gracefully. If location is denied, present an alert and clear the root template:
swift
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()
}
}
}优雅处理权限变更。若位置权限被拒绝,展示提示并清空根模板:
swift
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
)
}
]
)
// 先关闭已展示的模板
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()
}
}
}Order Status with Live Activities
基于Live Activities的订单状态
After a user places an order, start a Live Activity to show status on the Lock Screen. Live Activities don't display in CarPlay but provide glanceable updates:
swift
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
)用户下单后,启动Live Activity以在锁屏展示订单状态。Live Activities不会在CarPlay中显示,但可提供便捷的状态更新:
swift
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
)Listening for Updates
监听更新
Set up listeners for content updates, state changes, and push token updates. This is critical because quick-ordering apps spend time in the background — use push notifications for updates:
swift
// 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)
}
}设置监听器以监听内容更新、状态变更和推送令牌更新。这一点至关重要,因为快速点餐应用大多处于后台状态——需使用推送通知进行更新:
swift
// 内容更新
Task { @MainActor in
for await change in activity.contentUpdates {
try saveOrderState(state: change.state)
WidgetCenter.shared.reloadAllTimelines()
}
}
// 活动状态(结束/关闭)
Task { @MainActor in
for await state in activity.activityStateUpdates {
if state == .dismissed || state == .ended {
await activity.end(nil, dismissalPolicy: .immediate)
}
WidgetCenter.shared.reloadAllTimelines()
}
}
// 用于远程更新的推送令牌
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)
}
}Push Notification JWT
推送通知JWT
For server-side push notifications to update Live Activities, create a JWT using P256 signing:
swift
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: ".")若要通过服务器端推送通知更新Live Activities,需使用P256签名创建JWT:
swift
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: ".")Key Design Considerations
关键设计注意事项
- 12 POI limit — CarPlay displays a maximum of 12 points of interest at once
- Background updates — Use push notifications, not foreground polling, since quick-ordering apps spend most time in background
- Location is essential — Handle all authorization states gracefully; the app depends on location for relevant results
- Live Activities — They don't render in CarPlay, but provide Lock Screen status updates
- Stale dates — Set reasonable stale dates on Live Activity content (e.g., 30 minutes for food orders)
- Token management — Cache and refresh JWTs; listen for push token changes on the activity
- 12个POI限制 — CarPlay屏幕最多同时显示12个兴趣点
- 后台更新 — 由于快速点餐应用大多处于后台,需使用推送通知而非前台轮询
- 位置服务是核心 — 优雅处理所有权限状态;应用依赖位置信息提供相关结果
- Live Activities — 不会在CarPlay中渲染,但可在锁屏展示状态更新
- 过期时间 — 为Live Activity内容设置合理的过期时间(如餐饮订单设置30分钟)
- 令牌管理 — 缓存并刷新JWT;监听活动的推送令牌变更
See Also
相关链接
- — Map with selectable POIs
CPPointOfInterestTemplate - — Info display for orders, parking, charging
CPInformationTemplate - — Stylized action buttons
CPTextButton - Apple CarPlay Entitlement Request
- Sample Project Download
- — 带可选POI的地图组件
CPPointOfInterestTemplate - — 用于展示订单、停车、充电信息的模板
CPInformationTemplate - — 样式化操作按钮
CPTextButton - Apple CarPlay权限申请
- 示例项目下载