14、CarPlay仪表盘与Siri集成:仪表盘Widget开发、SiriKit意图定义、语音命令处理

说实话,CarPlay仪表盘这块,是我在车载开发中觉得最有意思的部分之一。你想想看,用户开车时最不想干的事就是盯着屏幕点点点。仪表盘Widget和Siri集成,说白了就是让用户「动口不动手」,甚至「瞄一眼就够了」。

我最早接触这个需求,是帮一家车厂做导航Widget。当时产品经理说:「用户就想在仪表盘上看到下一个转弯还有多远。」我心想这还不简单?结果一做才发现,坑不少。今天我把这些经验掰开揉碎了讲给你。

14.1 仪表盘Widget开发基础

CarPlay仪表盘,不是你想放什么就放什么。它有一套严格的模板和生命周期管理。我习惯把Widget理解成「一块会呼吸的卡片」——它得在有限的空间里,展示最核心的信息。

14.1.1 Widget的模板类型

Apple提供了几种现成的模板,我列个表给你看:

模板名称 适用场景 我的建议
CPDashboardButtonWidget 单个按钮操作(如回家、暂停) 适合高频操作,别放超过3个
CPDashboardManeuverWidget 导航转向提示 这个我踩过坑,后面细说
CPDashboardNowPlayingWidget 当前播放媒体 系统自带,一般不用自己写

嗯,这里要注意:Widget不是独立运行的。它依附于你的主App。如果主App被系统终止了,Widget也会消失。所以你得保证App在后台活着。

14.1.2 开发一个导航Widget

我拿导航Widget举例。你需要在 CPApplicationDelegate 里注册:

- (void)application:(CPApplication *)application didConnectDashboardController:(CPDashboardController *)dashboardController {
    // 创建导航Widget
    CPManeuver *nextManeuver = [[CPManeuver alloc] init];
    nextManeuver.instructionVariants = @[@"前方500米右转进入主路"];
    nextManeuver.symbolImage = [UIImage imageNamed:@"turn_right"];
    
    CPDashboardManeuverWidget *widget = [[CPDashboardManeuverWidget alloc] init];
    widget.leadingManeuver = nextManeuver;
    
    // 设置到仪表盘
    [dashboardController setWidget:widget];
}

这段代码看起来简单,但我曾经犯过一个低级错误:忘记设置 symbolImage。结果仪表盘上只显示文字,没有图标,用户根本来不及看。你想想看,开车时谁会读「前方500米右转」?瞟一眼箭头就够了。

避坑指南:Widget的图片必须用模板图片(Template Image),否则系统会强制渲染成白色。我曾经用了一张普通PNG,结果在深色模式下完全看不见。

14.2 SiriKit意图定义

Siri集成这块,我刚开始觉得挺玄乎的。后来发现,其实就是定义「用户想干什么」,然后告诉Siri怎么处理。

14.2.1 创建自定义意图

在Xcode里新建一个 .intentdefinition 文件。我习惯把意图分成两类:

  • 查询类:用户问「还有多久到家?」
  • 操作类:用户说「导航到公司」

举个例子,定义一个「查询剩余里程」的意图:

// IntentDefinition文件里
// 意图名称:CheckRemainingDistance
// 参数:destination(NSString)
// 响应:distance(NSNumber), unit(NSString)

// 在代码里处理
- (void)handleCheckRemainingDistance:(CheckRemainingDistanceIntent *)intent 
                          completion:(void (^)(CheckRemainingDistanceIntentResponse *))completion {
    
    NSString *destination = intent.destination;
    // 计算距离...
    NSInteger distance = [self calculateDistanceTo:destination];
    
    CheckRemainingDistanceIntentResponse *response = 
        [[CheckRemainingDistanceIntentResponse alloc] initWithCode:CheckRemainingDistanceIntentResponseCodeSuccess 
                                                     userActivity:nil];
    response.distance = @(distance);
    response.unit = @"公里";
    
    completion(response);
}

这里有个细节:响应里的参数名要和定义文件里完全一致。我有一回把 distance 写成了 remainingDistance,结果Siri死活不念出来。调试了半天才发现是命名不匹配。

14.2.2 意图的短语训练

定义好意图后,你得告诉Siri用户会怎么说。在 Info.plist 里配置:

<key>INIntentsSupported</key>
<array>
    <dict>
        <key>INIntentIdentifier</key>
        <string>CheckRemainingDistance</string>
        <key>INIntentPhrases</key>
        <array>
            <string>到{目的地}还有多远</string>
            <string>距离{目的地}还有多少公里</string>
        </array>
    </dict>
</array>

我个人习惯至少写3-5个短语变体。因为用户说话方式千奇百怪,有人会说「还有多远到公司」,有人会说「公司还有几公里」。你覆盖得越多,Siri识别越准。

小技巧:短语里的参数用花括号括起来,比如 {目的地}。Siri会自动匹配用户说的内容到这个参数上。

14.3 语音命令处理

语音命令处理,说白了就是「Siri把用户的话转成意图,你的App执行意图」。但这里有个关键点:处理速度。开车时用户等不了3秒。

14.3.1 处理流程

我画了一张流程图,帮你理清整个链路:

用户说出语音命令 Siri解析为意图 App处理意图 返回响应给Siri Siri播报结果

你看,整个链路其实不长。但最容易出问题的是第三步——App处理意图。我遇到过一种情况:用户说「导航到公司」,App需要先获取当前位置,再计算路线。如果网络慢,用户会以为Siri没反应。

14.3.2 异步处理与进度反馈

我的解决方案是:先给Siri一个「正在处理」的响应

- (void)handleNavigationToDestination:(NavigationToDestinationIntent *)intent 
                          completion:(void (^)(NavigationToDestinationIntentResponse *))completion {
    
    // 立即返回一个「处理中」的响应
    NavigationToDestinationIntentResponse *pendingResponse = 
        [[NavigationToDestinationIntentResponse alloc] initWithCode:NavigationToDestinationIntentResponseCodeContinueInApp 
                                                       userActivity:nil];
    completion(pendingResponse);
    
    // 然后在后台真正处理
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self calculateRouteTo:intent.destination completion:^(Route *route) {
            // 处理完成后,通过CPDashboardController更新Widget
            [self updateWidgetWithRoute:route];
        }];
    });
}

这里用到了 ContinueInApp 响应码。它的意思是:「Siri,你先别播报,让App来处理,处理完了App自己会更新界面。」这样用户不会听到「正在处理」这种废话,而是直接看到仪表盘上的路线更新了。

核心原则:语音命令的响应时间不要超过1.5秒。超过这个时间,用户就会觉得「这车机好卡」。我实测过,0.8秒以内是最理想的。

14.4 实战中的坑与经验

最后分享几个我踩过的坑,你遇到了可以少走弯路。

14.4.1 Widget的刷新时机

Widget不是实时刷新的。系统会按一定频率拉取数据。我一开始以为设置好数据源就行,结果发现用户开了5公里,Widget上的剩余距离还是原来的值。

解决办法:在数据变化时主动调用 [dashboardController setWidget:updatedWidget]。注意,是重新设置整个Widget,不是修改属性。

14.4.2 Siri的权限问题

Siri集成需要用户在设置里授权。如果用户没授权,你的意图永远不会被调用。我建议在App首次启动时弹窗引导用户开启:

// 检查Siri权限
[INPreferences requestSiriAuthorization:^(INSiriAuthorizationStatus status) {
    if (status == INSiriAuthorizationStatusAuthorized) {
        // 可以注册意图
    } else {
        // 引导用户去设置
    }
}];

嗯,这里要注意:不要每次启动都弹窗。用户会烦的。我习惯只在第一次启动时弹,之后如果用户没授权,就在设置页面留个入口。

14.4.3 多语言支持

如果你的App要出海,Siri的短语必须本地化。中文用户说「导航到公司」,英文用户说「Navigate to office」。在 .intentdefinition 文件里,每个短语都可以设置不同语言。

我吃过这个亏。当时只写了中文短语,结果美国用户用Siri说「Go home」,App完全没反应。后来补了英文短语才解决。

我的习惯:每次新增一个意图,立刻把支持的语言短语都写上。别想着「以后再说」,以后你会忘的。

好了,关于CarPlay仪表盘和Siri集成,核心内容就这些。说白了,Widget是「面子」,Siri是「里子」。面子要简洁直观,里子要快速响应。两者配合好了,用户才会觉得这车机「好用」。