第10章:CarPlay项目创建——配置Info.plist、实现CPApplicationDelegate、管理CPInterfaceController
好,咱们今天聊聊CarPlay项目的创建。说实话,我第一次做CarPlay项目时,以为跟普通iOS App差不多,结果一上来就被Info.plist给绊了一跤。后来才明白,CarPlay的启动流程和UI管理,跟手机端完全是两码事。
10.1 配置Info.plist——让系统认识你的CarPlay App
CarPlay App本质上还是个iOS App,但它需要向系统声明:“嘿,我支持CarPlay!”这个声明就在Info.plist里。
你需要添加一个键:UIApplicationSupportsMultipleScenes,并设为YES。为什么?因为CarPlay使用UIScene生命周期,而不是传统的AppDelegate。我个人习惯把这个键放在Info.plist的最前面,方便一眼看到。
然后,添加CPApplicationDelegateClassKey。这个键告诉系统,你的CarPlay代理类是谁。举个例子:
<key>CPApplicationDelegateClassKey</key>
<string>CarPlaySceneDelegate</string>
嗯,这里要注意:类名必须跟你在代码里实现的类完全一致,大小写都不能错。我曾经因为手抖把“CarPlaySceneDelegate”写成了“CarplaySceneDelegate”,结果调试了一下午……
另外,如果你的App同时支持手机和CarPlay,记得在UISceneConfigurations里分别配置。CarPlay的场景配置长这样:
<key>UISceneConfigurations</key>
<dict>
<key>CPTemplateApplicationSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>CarPlay Scene</string>
<key>UISceneDelegateClassName</key>
<string>CarPlaySceneDelegate</string>
</dict>
</array>
</dict>
说白了,Info.plist就是CarPlay App的“身份证”。配置不对,系统根本不认你。
10.2 实现CPApplicationDelegate——CarPlay的入口点
CarPlay不再使用传统的UIApplicationDelegate,而是用CPApplicationDelegate。这个协议里最重要的方法是:
application(_:didConnectCarInterfaceController:to:)——CarPlay连接时调用application(_:didDisconnectCarInterfaceController:from:)——CarPlay断开时调用
你想想看,这两个方法就像CarPlay的“插拔”事件。连接时,你拿到CPInterfaceController,开始构建UI;断开时,你清理资源。
我通常这样实现:
class CarPlaySceneDelegate: UIResponder, CPApplicationDelegate {
var interfaceController: CPInterfaceController?
func application(_ application: CPApplication,
didConnectCarInterfaceController interfaceController: CPInterfaceController,
to window: CPWindow) {
self.interfaceController = interfaceController
// 设置根模板
let listTemplate = CPListTemplate(title: "我的CarPlay", sections: [])
interfaceController.setRootTemplate(listTemplate, animated: true)
}
func application(_ application: CPApplication,
didDisconnectCarInterfaceController interfaceController: CPInterfaceController,
from window: CPWindow) {
self.interfaceController = nil
// 清理工作
}
}
这里有个坑:didConnectCarInterfaceController可能会被多次调用。比如用户拔掉手机再插上。所以,我建议在里面加个判断,防止重复设置根模板。
setRootTemplate,结果界面死活不刷新。后来加了DispatchQueue.main.async才搞定。
10.3 管理CPInterfaceController——CarPlay的导航中枢
CPInterfaceController是CarPlay的“导航控制器”。它管理着模板的堆栈,类似UINavigationController。但区别在于,CarPlay的模板是“全屏”的,你不能像手机那样随意push/pop。
常用的操作有:
setRootTemplate(_:animated:)——设置根模板pushTemplate(_:animated:)——推入新模板popTemplate(animated:)——弹出当前模板presentTemplate(_:animated:)——模态展示模板dismissTemplate(animated:)——关闭模态模板
我个人习惯把CPInterfaceController的引用保存在单例或场景代理里,方便全局访问。但要注意,不要强引用它,否则可能造成循环引用。
来看一个实际例子:用户点击列表项,进入详情页。
func listTemplate(_ listTemplate: CPListTemplate, didSelect item: CPListItem) {
guard let interfaceController = self.interfaceController else { return }
let detailTemplate = CPInformationTemplate(title: "详情",
layout: .leading,
items: [],
actions: [])
interfaceController.pushTemplate(detailTemplate, animated: true)
}
你可能会问:为什么不用presentTemplate?嗯,这取决于你的设计。push是导航栈的一部分,用户可以通过返回按钮回去;present是模态的,用户必须手动关闭。我一般用push做层级导航,用present做临时弹窗。
setRootTemplate重置栈。
10.4 实战:完整的CarPlay启动流程
把上面三块拼起来,就是一个完整的CarPlay App启动流程:
- 系统读取Info.plist,找到
CPApplicationDelegateClassKey - 实例化你的
CarPlaySceneDelegate - 调用
application(_:didConnectCarInterfaceController:to:) - 你在方法里设置根模板
- 用户开始交互
- 断开时调用
application(_:didDisconnectCarInterfaceController:from:)
这个流程看起来简单,但每个环节都有细节。比如,Info.plist配置错了,系统根本不会调用你的代理方法。再比如,你在didConnect里做了耗时操作,用户会看到黑屏。
我建议在didConnect里只做必要的初始化,比如设置根模板。其他数据加载,放到模板的viewDidLoad或异步任务里。
10.5 知识体系图
下面这张图展示了CarPlay项目创建的核心逻辑:
这张图把整个流程串起来了。从配置开始,到代理实现,再到界面管理,最后呈现给用户。每一步都依赖上一步,缺一不可。
10.6 避坑指南
最后,分享几个我踩过的坑:
- Info.plist大小写敏感——键名必须完全匹配,包括大小写。我建议直接复制官方文档的字符串。
- 不要在主线程做耗时操作——CarPlay的UI渲染很吃资源,如果你在
didConnect里做网络请求,用户会看到明显的卡顿。 - 模板栈深度限制——超过10层会crash。我一般用
popToRootTemplate来清理栈。 - 模拟器 vs 真机——CarPlay模拟器跟真机行为有差异。比如模拟器上
pushTemplate动画很流畅,真机上可能掉帧。所以,一定要在真机上测试。
好了,这一章的内容就这些。配置好Info.plist,实现代理,管理好界面控制器,你的CarPlay App就能跑起来了。下一章咱们聊聊具体的模板类型,比如列表模板、地图模板,到时候再细说。
公众号:蓝海资料掘金营,微信deep3321