第十三章:CarPlay通讯应用开发:CPContactTemplate、拨号与消息、语音集成

通讯应用,说白了就是CarPlay上最刚需的那类App。你想想看,用户开车时最想干的事是什么?打电话、发消息、听语音。这一章我们就来啃这块硬骨头。

我个人习惯把CarPlay通讯开发拆成三个核心模块:联系人展示、拨号交互、语音集成。这三个模块分别对应CPContactTemplate、拨号API和Siri授权。咱们一个一个来。

13.1 CPContactTemplate:联系人卡片模板

CPContactTemplate是CarPlay专门为通讯应用设计的模板。它不像列表模板那样只能展示一堆名字,而是可以展示一个联系人的完整信息卡片。

我记得第一次用这个模板时,最让我惊讶的是它自带“拨号”和“消息”按钮。你不需要自己画按钮,系统帮你搞定了。

13.1.1 创建联系人卡片

先看代码,怎么创建一个联系人卡片:

// 创建联系人
CPContact *contact = [[CPContact alloc] init];
contact.name = @"张三";
contact.image = [UIImage imageNamed:@"zhangsan_avatar"];

// 添加电话号码
CPContactPhoneNumber *phone = [CPContactPhoneNumber 
    phoneNumberWithPhoneNumber:@"13800138000" 
    label:@"手机"];
contact.phoneNumbers = @[phone];

// 添加邮箱
CPContactEmailButton *email = [CPContactEmailButton 
    emailButtonWithEmail:@"zhangsan@example.com"];
contact.emailButtons = @[email];

// 创建模板
CPContactTemplate *contactTemplate = [[CPContactTemplate alloc] 
    initWithContact:contact];
contactTemplate.tabTitle = @"联系人";
contactTemplate.tabImage = [UIImage systemImageNamed:@"person.circle"];

这里有个坑。我在项目中遇到过,contact.image如果传nil,系统会显示一个默认的灰色头像。但如果你传了一个尺寸不对的图片,它会被拉伸得很难看。建议图片尺寸统一为120x120pt。

13.1.2 处理按钮事件

联系人卡片上的按钮点击后,你需要实现代理方法:

// 实现 CPContactTemplateDelegate
- (void)contactTemplate:(CPContactTemplate *)contactTemplate 
    didSelectPhoneNumber:(CPContactPhoneNumber *)phoneNumber {
    // 用户点击了电话号码
    [self startCallWithNumber:phoneNumber.phoneNumber];
}

- (void)contactTemplate:(CPContactTemplate *)contactTemplate 
    didSelectMessageButton:(CPContactMessageButton *)messageButton {
    // 用户点击了消息按钮
    [self composeMessageTo:messageButton.phoneNumber];
}
小技巧: 如果你不想显示某个按钮,比如不想显示邮箱按钮,直接把emailButtons设为空数组就行。系统会自动隐藏对应的按钮区域。

13.2 拨号功能实现

拨号功能是通讯应用的核心。CarPlay提供了两种拨号方式:一种是直接用系统电话,另一种是通过VoIP。

13.2.1 使用系统电话

最简单的方式,直接调用系统电话:

// 拨打电话
- (void)startCallWithNumber:(NSString *)phoneNumber {
    NSString *telURL = [NSString stringWithFormat:@"tel://%@", phoneNumber];
    NSURL *url = [NSURL URLWithString:telURL];
    
    if ([[UIApplication sharedApplication] canOpenURL:url]) {
        [[UIApplication sharedApplication] openURL:url 
            options:@{} completionHandler:nil];
    }
}

嗯,这里要注意。在CarPlay上调用openURL,系统会自动切换到电话界面。通话结束后,用户需要手动切回你的App。这个体验其实不太好,但没办法,系统限制。

13.2.2 使用CallKit实现VoIP

如果你有自己的VoIP服务,可以用CallKit来集成。这样用户体验会好很多:

// 配置CallKit
CXProviderConfiguration *config = [[CXProviderConfiguration alloc] 
    initWithLocalizedName:@"我的通讯"];
config.supportsVideo = NO;
config.maximumCallGroups = 1;
config.maximumCallsPerCallGroup = 1;

CXProvider *provider = [[CXProvider alloc] initWithConfiguration:config];
[provider setDelegate:self queue:nil];

// 发起呼叫
NSUUID *callUUID = [NSUUID UUID];
CXStartCallAction *action = [[CXStartCallAction alloc] 
    initWithCallUUID:callUUID handleValue:phoneNumber];
CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
[provider reportNewIncomingCallWithUUID:callUUID 
    update:[[CXCallUpdate alloc] init] completion:^(NSError * _Nullable error) {
    if (error) {
        NSLog(@"呼叫失败: %@", error.localizedDescription);
    }
}];
警告: 使用CallKit需要申请权限。在Info.plist中必须添加com.apple.developer.calls权限,否则你的App在CarPlay上根本不会显示拨号界面。

13.3 消息功能集成

消息功能比拨号复杂一些。因为CarPlay上的消息输入,主要靠语音。

13.3.1 发送消息

CarPlay提供了CPMessageComposeBar,但说实话,这个组件限制很多。我建议直接用自定义界面:

// 创建消息发送界面
- (void)composeMessageTo:(NSString *)phoneNumber {
    // 使用SFSpeechRecognizer进行语音识别
    SFSpeechRecognizer *recognizer = [[SFSpeechRecognizer alloc] 
        initWithLocale:[NSLocale localeWithLocaleIdentifier:@"zh-CN"]];
    
    // 请求语音识别权限
    [SFSpeechRecognizer requestAuthorization:^(SFSpeechRecognizerAuthorizationStatus status) {
        if (status == SFSpeechRecognizerAuthorizationStatusAuthorized) {
            // 开始语音识别
            [self startVoiceRecognitionWithRecognizer:recognizer];
        }
    }];
}

13.3.2 消息模板

如果你不想自己写界面,可以用CPMessageListItem来展示消息列表:

// 创建消息列表项
CPMessageListItem *item = [[CPMessageListItem alloc] 
    initWithConversationIdentifier:@"conv_001" 
    text:@"明天下午三点见" 
    date:[NSDate date]];
item.trailingText = @"张三";
item.leadingImage = [UIImage systemImageNamed:@"person.circle.fill"];

// 添加到列表模板
CPListTemplate *listTemplate = [[CPListTemplate alloc] 
    initWithTitle:@"消息" sections:@[section]];
[self pushTemplate:listTemplate animated:YES];
核心要点: 消息列表的conversationIdentifier必须唯一。我踩过这个坑,如果两个会话用了同一个ID,CarPlay会合并显示,导致用户点进去看到的是别人的消息。

13.4 语音集成:Siri与听写

语音是CarPlay上最重要的交互方式。没有之一。用户开车时不可能低头打字,全靠语音。

13.4.1 集成Siri意图

想让Siri能调用你的App,需要注册Intents:

// 在Info.plist中注册
// INStartAudioCallIntent
// INStartVideoCallIntent
// INSendMessageIntent

// 实现INStartAudioCallIntentHandling
- (void)handleStartAudioCall:(INStartAudioCallIntent *)intent 
    completion:(void (^)(INStartAudioCallIntentResponse *))completion {
    
    INPerson *person = intent.contacts.firstObject;
    NSString *phoneNumber = person.personHandle.value;
    
    // 发起呼叫
    [self startCallWithNumber:phoneNumber];
    
    INStartAudioCallIntentResponse *response = [[INStartAudioCallIntentResponse alloc] 
        initWithCode:INStartAudioCallIntentResponseCodeSuccess 
        userActivity:nil];
    completion(response);
}

我曾经遇到一个问题:Siri识别到了联系人,但就是调不起我的App。后来发现是Intents的plist配置里少了一个INStartAudioCallIntent的Supported Intents声明。这个坑卡了我半天。

13.4.2 语音听写

除了Siri,你还可以用SFSpeechRecognizer做实时听写:

// 配置语音识别
SFSpeechRecognizer *recognizer = [[SFSpeechRecognizer alloc] init];
recognizer.defaultTaskHint = SFSpeechRecognitionTaskHintDictation;

// 创建音频引擎
AVAudioEngine *audioEngine = [[AVAudioEngine alloc] init];
AVAudioInputNode *inputNode = audioEngine.inputNode;

// 开始识别
SFSpeechRecognitionTask *task = [recognizer 
    recognitionTaskWithRequest:request 
    resultHandler:^(SFSpeechRecognitionResult * _Nullable result, 
                    NSError * _Nullable error) {
    if (result) {
        NSString *transcript = result.bestTranscription.formattedString;
        NSLog(@"识别结果: %@", transcript);
        // 更新UI
        [self updateMessageText:transcript];
    }
}];
提示: 语音识别在CarPlay上有个特点:车速超过100km/h时,识别准确率会明显下降。这不是代码问题,是风噪导致的。建议在车速较高时,提示用户使用预设短语。

13.5 知识体系总览

说了这么多,咱们用一张图来梳理一下整个通讯应用的知识结构:

CarPlay通讯应用知识体系 联系人展示 拨号交互 消息功能 CPContactTemplate 联系人卡片布局 按钮事件处理 系统电话 (tel://) CallKit VoIP集成 权限配置 CPMessageListItem 语音听写输入 消息列表展示 语音集成:Siri意图 + 实时听写 三个模块通过语音集成串联,形成完整通讯体验

13.6 避坑指南

最后,分享几个我实际开发中踩过的坑:

  • 联系人头像加载慢: CarPlay的屏幕分辨率其实不高,但图片加载如果卡顿,用户体验会很差。建议预加载头像,或者用缩略图。
  • 拨号后无法返回: 用系统电话拨号后,用户需要手动点击屏幕上的“返回”按钮。这个没法避免,但可以在拨号前提示用户。
  • 语音识别权限: 用户第一次使用语音时,必须弹窗请求权限。如果用户拒绝了,你的App在CarPlay上基本就废了一半。建议在App首次启动时就引导用户授权。
  • 多语言支持: CarPlay在不同国家显示的语言不同。你的联系人姓名、电话号码格式都要做本地化处理。我见过一个App,在美国显示正常,到了日本就乱码了。

嗯,通讯应用开发大概就是这些内容。说白了,CarPlay上的通讯App,核心就是联系人、拨号、消息这三个模块,再加上语音这个粘合剂。把这几块搞定了,你的App在车上就能跑得很顺畅。

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