WWDC Quick Look 💓 By SwiftGGTeam
Accelerate your app with CarPlay

Accelerate your app with CarPlay

观看原视频

Highlight

iOS 14 将 CarPlay framework 的模板从导航扩展到音频、通信、EV 充电、停车和快餐点餐应用,开发者可以用 UIScene、预定义模板和单一类别 entitlement 把车内任务做成几秒内完成的车屏体验。

核心内容

CarPlay 以前的第三方框架能力主要服务导航 App。音频 App 依赖 playable content API,通信 App 依赖 SiriKit 和 CallKit,停车、充电、快餐点餐这类和开车直接相关的任务还不能用同一套 CarPlay 模板放到车屏上。结果是,很多本该在车里完成的操作仍然回到手机,驾驶场景不适合这样做。

iOS 14 的变化是给这些类别开放新的 CarPlay templates。Apple 把问题限定得很窄:CarPlay 是给驾驶员用的,界面要围绕最常见的车内任务,交互要短,信息要能扫一眼完成判断。账号设置、完整菜单、复杂管理页都应该留在 iPhone 上。

对开发者来说,这不是把 iPhone App 搬到车机屏幕。CarPlay App 运行在 iPhone 上,通过 CarPlay framework 把预定义模板显示到车屏。App 需要采用 UIScene,声明 CarPlay scene,在连接车屏时拿到 CPInterfaceController,再把列表、标签栏、Now Playing、POI、信息页这些模板组织成安全的车内流程。

Session 后半段把新类别落到具体场景。快餐点餐 App 可以展示附近门店、历史订单、收藏和订单信息;EV 充电和停车 App 可以用地图找地点、检查可用性,并继续进入选择、导航或摘要页面。最后还提醒开发者,CarPlay 需要申请 entitlement,而且 App 必须选择单一类别。

详细内容

CarPlay scene 从 Info.plist 开始

04:24)使用 CarPlay framework 的前提是采用 UIScene。App 要在 UIApplicationSceneManifest 里额外声明一个 CarPlay template application scene,并指定对应的 scene delegate。这个配置和 iPhone scene 并存,用户从 CarPlay 主屏幕启动 App 时,系统连接的是车屏 scene。

// CarPlay Scene Manifest

<key>UIApplicationSceneManifest</key>
<dict>
    <key>UISceneConfigurations</key>
	<dict>
		<key>CPTemplateApplicationSceneSessionRoleApplication</key>
		<array>
			<dict>
				<key>UISceneClassName</key>
				<string>CPTemplateApplicationScene</string>
				<key>UISceneConfigurationName</key>
				<string>MyApp—Car</string>
				<key>UISceneDelegateClassName</key>
				<string>MyApp.CarPlaySceneDelegate</string>
			</dict>
		</array>
	</dict>
</dict>

关键点:

  • UIApplicationSceneManifest 是 UIScene 的入口,CarPlay 配置放在这里。
  • CPTemplateApplicationSceneSessionRoleApplication 声明这是 CarPlay 模板应用 scene。
  • CPTemplateApplicationScene 让系统为车屏创建 CarPlay scene。
  • MyApp.CarPlaySceneDelegate 是后续接收 CarPlay 连接和断开的 delegate。

连接车屏后设置根模板

05:12)CarPlay scene delegate 的 didConnect 会在 App 启动到车屏时调用。开发者需要保存 CPInterfaceController,因为之后 push、set root、更新模板都靠它完成。示例里先构造一个列表,再把它设置成根模板。

// CarPlay App Lifecycle

import CarPlay

class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate {
    var interfaceController: CPInterfaceController?

    func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene,
            didConnect interfaceController: CPInterfaceController) {

        self.interfaceController = interfaceController
        let item = CPListItem(text: "Rubber Soul", detailText: "The Beatles")
        let section = CPListSection(items: [item])
        let listTemplate = CPListTemplate(title: "Albums", sections: [section])
        interfaceController.setRootTemplate(listTemplate, animated: true)
    }

  func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene,
            didDisconnect interfaceController: CPInterfaceController) {
    self.interfaceController = nil
}

关键点:

  • CPTemplateApplicationSceneDelegate 是 CarPlay scene 生命周期的入口。
  • didConnect interfaceController 表示车屏已经连接,可以开始显示模板。
  • CPListItemCPListSectionCPListTemplate 组合出一个列表界面。
  • setRootTemplate 把模板放到车屏栈的根部。
  • didDisconnect 释放 interfaceController,避免继续操作已经断开的车屏。

音频 App 从 playable content 迁移到模板

05:54)iOS 14 里 playable content 会被废弃,但旧系统仍可继续使用它。新的音频模板让 App 自己构造列表、标签栏和 Now Playing 体验,iOS 13 及更早版本可以继续走 playable content。

// CPListTemplate

import CarPlay

let item = CPListItem(text: "Rubber Soul", detailText: "The Beatles")
let section = CPListSection(items: [item])
let listTemplate = CPListTemplate(title: "Albums", sections: [section])
self.interfaceController.pushTemplate(listTemplate, animated: true)

关键点:

  • CPListItem 对应列表里的单行内容,适合专辑、播放列表、消息线程这类对象。
  • CPListSection 把多个 item 组织到同一个列表区块。
  • CPListTemplate 是 CarPlay 最基础的模板之一。
  • pushTemplate 用于从当前页面进入下一层模板。

06:09)列表项被选中时,CarPlay 会调用 listItemHandler。handler 里可以先启动播放或配置下一个模板,再调用 completion()。如果异步工作没有立刻完成,CarPlay 会在列表项上显示 spinner,直到 completion 被调用。

// CPListTemplate

import CarPlay

let item = CPListItem(text: "Rubber Soul", detailText: "The Beatles")
item.listItemHandler = { item, completion, [weak self] in
    // Start playback, then...
    self?.interfaceController.pushTemplate(CPNowPlayingTemplate.shared, animated: true)
    completion()
}

// Later...
item.image = ...

关键点:

  • listItemHandler 是用户点选列表项后的入口。
  • handler 接收被选中的 item 和必须调用的 completion
  • 示例在启动播放后 push 到共享的 CPNowPlayingTemplate
  • completion() 通知 CarPlay 当前选择处理完成,可以移除 busy 状态。
  • item.image = ... 对应 iOS 14 新增的动态列表项更新能力。

07:58CPTabBarTemplate 是 iOS 14 新增的容器模板。它把多个模板变成 CarPlay 用户熟悉的标签栏界面,并允许动态增加、移除、重排标签,或更新某个标签上的 badge。

// CPTabBarTemplate

import CarPlay

let item = CPListItem(text: "Rubber Soul", detailText: "The Beatles")
let section = CPListSection(items: [item])
let favorites = CPListTemplate(title: "Albums", sections: [section])
favorites.tabSystemItem = .favorites
favorites.showsTabBadge = true

let albums: CPGridTemplate = ...
albums.tabTitle = "Albums"
albums.tabImage = ...

let tabBarTemplate = CPTabBarTemplate(templates: [favorites, albums])
self.interfaceController.setRootTemplate(tabBarTemplate, animated: false)

// Later...
favorites.showsTabBadge = false
tabBarTemplate.updateTemplates([favorites, albums])

关键点:

  • favoritesalbums 是标签栏里的两个模板。
  • tabSystemItem 使用系统提供的标签栏图标。
  • showsTabBadge 可以在模板标签上显示状态提示。
  • CPTabBarTemplate(templates:) 把多个模板装进同一个容器。
  • updateTemplates 支持运行时更新标签栏内容和 badge 状态。

09:34)音频列表在 iOS 14 获得更多展示能力。CPListImageRowItem 可以在一行里展示一组封面图,每张图都能独立响应选择。Apple Books 用它在 CarPlay 上展示最近的有声书。

// List Items for Audio Apps

import CarPlay

let gridImages: [UIImage] = ...
let imageRowItem = CPListImageRowItem(text: "Recent Audiobooks", images: gridImages)

imageRowItem.listItemHandler = { item, completion in
    print("Selected image row header!")
    completion()
}

imageRowItem.listImageRowHandler = { item, index, completion in
    print("Selected artwork at index \(index)!")
    completion()
}

let section = CPListSection(items: [imageRowItem])
let listTemplate = CPListTemplate(title: "Listen Now", sections: [section])
self.interfaceController.pushTemplate(listTemplate, animated: true)

关键点:

  • CPListImageRowItem 用一组图片表达一组可选内容。
  • listItemHandler 处理整行标题被选中的情况。
  • listImageRowHandler 处理某一张封面被选中的情况,index 指出具体位置。
  • 两个 handler 都要在异步工作结束后调用 completion()

12:50)Now Playing 是共享模板。系统可能在用户点 CarPlay 主屏幕的 Now Playing 按钮时直接启动 App 并展示它,所以音频 App 应该在 CarPlay scene 连接后尽快配置 CPNowPlayingTemplate.shared

// Now Playing Template

import CarPlay

class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate {

    func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene,
            didConnect interfaceController: CPInterfaceController) {

        let nowPlayingTemplate = CPNowPlayingTemplate.shared

        let rateButton = CPNowPlayingPlaybackRateButton() { button in

            // Change the playback rate!

        }
        nowPlayingTemplate.updateNowPlayingButtons([rateButton])
    }
}

关键点:

  • CPNowPlayingTemplate.shared 表示 App 内只有一个 Now Playing 模板实例。
  • 初始化应尽早完成,因为系统可以替 App 展示这个模板。
  • CPNowPlayingPlaybackRateButton 给 Now Playing 底部增加播放速度按钮。
  • updateNowPlayingButtons 更新 Now Playing 可见按钮。

通信 App 继续依赖 SiriKit 和 CallKit

14:07)通信 App 在 iOS 14 也能使用 CarPlay framework 显示联系人、消息列表和消息状态,但语音和电话能力仍然要通过 SiriKit 与 CallKit 提供。新的 CPMessageListItem 用于消息线程列表,用户点按后,Siri 会根据 item 参数自动进入撰写、朗读或回复流程,而不是调用普通列表 handler。

16:12)通信类别还新增了 contact template。它可以显示联系人图片、最多三行描述文字、最多四个动作按钮和导航栏按钮。这个模板把地址簿里的核心操作放到车屏上,但仍然要求流程短,避免把完整通讯录管理搬进 CarPlay。

POI 模板服务停车、充电和快餐点餐

17:52)EV 充电、停车和快餐点餐的第一个任务通常是选地点。iOS 14 新增的 CPPointOfInterestTemplate 结合 MapKit 地图和信息面板,App 提供最多 12 个 CPPointOfInterest,CarPlay 负责地图显示、平移缩放和靠近点位的自动聚合。

19:46)当地图区域变化时,App 应该重新计算附近地点,并把新结果交给模板。这样用户移动、平移或缩放地图时,车屏上的地点列表会跟着当前区域更新。

// CPPointOfInterestTemplateDelegate

func pointOfInterestTemplate(_ template: CPPointOfInterestTemplate,
                             didChangeMapRegion region: MKCoordinateRegion) {

    self.locationManager.locations(for: region) { locations in
        template.setPointsOfInterest(locations, selectedIndex: 0)
    }
}

关键点:

  • CPPointOfInterestTemplateDelegate 接收地图区域变化。
  • didChangeMapRegion 给出新的 MKCoordinateRegion
  • App 可以查询自己的地点库,也可以结合 MapKit 查找附近目标。
  • setPointsOfInterest 用新地点替换模板上的列表和地图点位。

20:23)每个地点都要转成 CPPointOfInterest。Session 示例把应用自己的模型映射成标题、副标题、说明文字和图片,并尽量复用已有模型,减少模板更新成本。

// CPPointOfInterest creation

func locations(for region: MKCoordinateRegion,
               handler: ([CPPointOfInterest]) -> Void) {
    var tempateLocations: [CPPointOfInterest] = []

    for clientModel in self.executeQuery(for: region) {
        let templateModel : CPPointOfInterest = self.locations[clientModel.mapItem] ??
                CPPointOfInterest(location: clientModel.mapItem,
                                  title: clientModel.title,
                                  subtitle: clientModel.subtitle,
                                  informativeText: clientModel.informativeText,
                                  image: clientModel.mapImage)


        tempateLocations.append(templateModel)
    }
    handler(templateLocations)
}

关键点:

  • locations(for region:) 按当前地图区域生成 POI 列表。
  • executeQuery(for:) 代表 App 自己的附近地点查询。
  • CPPointOfInterest 汇总地图位置、标题、副标题、说明和图片。
  • 示例复用 self.locations[clientModel.mapItem],避免每次刷新都重建所有模型。

21:05)地点被选中后,信息面板可以显示最多两个按钮。示例里 CPPointOfInterestButton 用来选择某个地点,并更新图钉图片和 selectedIndex,让车屏立刻反映新的选中状态。

// Point of Interest Template location selection

let primaryButton = CPPointOfInterestButton(title: "Select") { button, [weak self] in
            let selectedIndex = ...

            if selectedIndex != NSNotFound {
                // Remove any existing selected state on previous location
                self?.selectedLocation.image = defaultMapImage
                // Change annotation for selected POI
                self?.selectedLocation = templateModel
                templateModel.image = selectedMapImage
                // Update the template with new values
                self?.pointOfInterestTemplate.selectedIndex = selectedIndex
            }
        }

let templateModel: CPPointOfInterest = ...

templateModel.primaryButton = primaryButton

关键点:

  • CPPointOfInterestButton 把地点卡片上的动作绑定到 handler。
  • selectedIndex 是当前选中地点的位置。
  • 更新 image 可以改变地图标注的视觉状态。
  • 设置 pointOfInterestTemplate.selectedIndex 会把选择状态同步到车屏。

信息模板完成订单、停车或充电摘要

22:29CPInformationTemplate 用于展示文本并接收用户响应。它支持一列或两列标签,以及 footer buttons。快餐点餐 App 可以用它显示订单摘要或确认页,EV 充电 App 可以用它展示充电站的重要信息。

24:38)发布前还要处理 entitlement。CarPlay App 必须是单一类别,所选 entitlement 会决定 App 能使用哪些 CarPlay templates。音频 App 如果还要支持 iOS 13 或更早版本,可以让 playable content 和 iOS 14 的音频模板共存。

核心启发

车载音频首页

做什么:把音乐、有声书或播客 App 的 CarPlay 首页改成标签栏,包含收藏、专辑、最近播放和 Now Playing。

为什么值得做:iOS 14 的音频模板让 App 从 playable content 的系统生成树状界面,转向自己组织列表、图片行和共享 Now Playing。

怎么开始:采用 UIScene,声明 CarPlay scene,用 CPListTemplate 做列表,用 CPListImageRowItem 展示封面,用 CPNowPlayingTemplate.shared 在连接车屏时配置播放按钮。

停车场查找和选择

做什么:在 CarPlay 上显示附近停车场,让驾驶员看价格、距离或可用信息,然后选择一个地点继续导航。

为什么值得做:Session 明确把停车列为 iOS 14 首次支持的 CarPlay 类别,并展示 POI 模板如何处理地图区域变化和地点选择。

怎么开始:申请 parking 类别 entitlement,用 CPPointOfInterestTemplate 提供最多 12 个相关地点,在 delegate 里根据 MKCoordinateRegion 刷新列表。

EV 充电站入口

做什么:给充电 App 增加车屏入口,显示附近充电站、状态摘要和选择按钮。

为什么值得做:EV charging 是 iOS 14 新增的 CarPlay 类别,POI 模板和信息模板正好覆盖找站点、看信息、确认下一步。

怎么开始:把充电站模型映射成 CPPointOfInterest,用 informativeText 放最关键的状态信息,再用 CPInformationTemplate 展示站点详情或确认页。

快餐取餐流程

做什么:让快餐 App 在 CarPlay 上显示附近门店、历史订单、收藏和当前订单摘要。

为什么值得做:演讲里的 quick-service restaurant 示例强调一键访问常见任务,不展示详细菜单、账号管理或设置。

怎么开始:把 CarPlay 体验限定为复购和取餐,用 tab bar 组织门店、历史订单和收藏,用信息模板展示订单摘要。

通信消息状态面板

做什么:为消息或通话 App 做一个 CarPlay 联系人和消息线程入口。

为什么值得做:iOS 14 允许通信 App 使用 CarPlay framework 显示联系人、消息列表和状态,但语音与通话仍由 SiriKit 和 CallKit 承担。

怎么开始:继续维护 SiriKit 与 CallKit 能力,用 CPMessageListItem 表达消息线程状态,用 contact template 提供联系人图片、描述和动作按钮。

关联 Session

评论

GitHub Issues · utterances