第十三章: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);
}
}];
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];
}
}];
13.5 知识体系总览
说了这么多,咱们用一张图来梳理一下整个通讯应用的知识结构:
13.6 避坑指南
最后,分享几个我实际开发中踩过的坑:
- 联系人头像加载慢: CarPlay的屏幕分辨率其实不高,但图片加载如果卡顿,用户体验会很差。建议预加载头像,或者用缩略图。
- 拨号后无法返回: 用系统电话拨号后,用户需要手动点击屏幕上的“返回”按钮。这个没法避免,但可以在拨号前提示用户。
- 语音识别权限: 用户第一次使用语音时,必须弹窗请求权限。如果用户拒绝了,你的App在CarPlay上基本就废了一半。建议在App首次启动时就引导用户授权。
- 多语言支持: CarPlay在不同国家显示的语言不同。你的联系人姓名、电话号码格式都要做本地化处理。我见过一个App,在美国显示正常,到了日本就乱码了。
嗯,通讯应用开发大概就是这些内容。说白了,CarPlay上的通讯App,核心就是联系人、拨号、消息这三个模块,再加上语音这个粘合剂。把这几块搞定了,你的App在车上就能跑得很顺畅。