第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可能会被多次调用。比如用户拔掉手机再插上。所以,我建议在里面加个判断,防止重复设置根模板。

注意:CarPlay的UI操作必须在主线程进行。我曾经在后台线程调用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做临时弹窗。

小技巧:CarPlay的模板栈最多支持10层。超过这个数,系统会拒绝push。所以,如果你的导航层级很深,考虑用setRootTemplate重置栈。

10.4 实战:完整的CarPlay启动流程

把上面三块拼起来,就是一个完整的CarPlay App启动流程:

  1. 系统读取Info.plist,找到CPApplicationDelegateClassKey
  2. 实例化你的CarPlaySceneDelegate
  3. 调用application(_:didConnectCarInterfaceController:to:)
  4. 你在方法里设置根模板
  5. 用户开始交互
  6. 断开时调用application(_:didDisconnectCarInterfaceController:from:)

这个流程看起来简单,但每个环节都有细节。比如,Info.plist配置错了,系统根本不会调用你的代理方法。再比如,你在didConnect里做了耗时操作,用户会看到黑屏。

我建议在didConnect里只做必要的初始化,比如设置根模板。其他数据加载,放到模板的viewDidLoad或异步任务里。

10.5 知识体系图

下面这张图展示了CarPlay项目创建的核心逻辑:

CarPlay项目创建核心流程 Info.plist 配置 CPApplicationDelegate 实现 CPInterfaceController 管理 模板展示与交互 声明CarPlay支持 处理连接/断开事件 管理模板栈 用户可见的UI

这张图把整个流程串起来了。从配置开始,到代理实现,再到界面管理,最后呈现给用户。每一步都依赖上一步,缺一不可。

10.6 避坑指南

最后,分享几个我踩过的坑:

  • Info.plist大小写敏感——键名必须完全匹配,包括大小写。我建议直接复制官方文档的字符串。
  • 不要在主线程做耗时操作——CarPlay的UI渲染很吃资源,如果你在didConnect里做网络请求,用户会看到明显的卡顿。
  • 模板栈深度限制——超过10层会crash。我一般用popToRootTemplate来清理栈。
  • 模拟器 vs 真机——CarPlay模拟器跟真机行为有差异。比如模拟器上pushTemplate动画很流畅,真机上可能掉帧。所以,一定要在真机上测试。

好了,这一章的内容就这些。配置好Info.plist,实现代理,管理好界面控制器,你的CarPlay App就能跑起来了。下一章咱们聊聊具体的模板类型,比如列表模板、地图模板,到时候再细说。


公众号:蓝海资料掘金营,微信deep3321