iOS12 Siri ShortCuts 应用 (二)
上一篇文章 iOS12 Siri ShortCuts 应用 (一) 主要写了通过 NSUserActivity 实现 Siri ShortCuts 的方案。具体功能就是通过用户自定义的siri 语音指令, 利用siri打开app, 完成用户想要的功能。
今天这篇文章就来介绍另外一种功能,通过 Intents Extension 实现不打开app 去完成某个任务。先来看下效果:
SiriKit Intent Definition File
在新建Extension之前,我们要通过 file->newfile, 选择 SiriKit Intent Definition File。创建好后是这个样子的:
上图中,我一共自定义了三个 Intent, 分别是 DailyPunch、BreakfastPunch和 SportPunch。在这个界面,你可以设置 Intent 的 category,title和一些参数。每个 Intent 都对应一个 Response, 如下图所示。你可以在response定义参数和错误类型。如图所示,我定义了 errorMessage 的参数 和 failureUnLogin等两个错误类型。这里 errorMessage 用来传递服务器返回的错误信息。
创建完成后,编译一下项目,xcode 会自动生成对应的类,我这里的话会生成 DailyPunchIntent 等三个类,每个类包含了 DailyPunchIntentHandling 协议和 DailyPunchIntentResponse 类等所需要的内容。
需要注意的是,这些类不会出现在项目的目录中,有点和 Core Data 类似。
但你可以正常使用,可以为其新建 Category 或者导入头文件就可以直接使用。
我这里通过 Category 为每个 Intent 类添加了 suggestedInvocationPhrase 属性,可以在用户录制的时候给出建议短语。
@implementation DailyPunchIntent (PXDailyPunch)
- (instancetype)init{
self = [super init];
if (self) {
self.suggestedInvocationPhrase = @"打卡";
}
return self;
}
@end
Intents Extension
接下来就是创建 Extension 了。通过 file -> new -> target , 选择 Intents Extension 即可。为了让 Extension 的界面便于控制,我选择了 Include UI Extension。这样就同时创建了两个Extension。
Intents Extension 创建好后,会自动出现一个名为 IntentHandler 的类。对于非即时通讯类的需求,可以删除其他的方法,保留下面一个方法即可:
- (id)handlerForIntent:(INIntent *)intent {
if ([intent isKindOfClass:[DailyPunchIntent class]]) {
DailyPunchIntentHandler *intentHandler = [[DailyPunchIntentHandler alloc] init];
return intentHandler;
}else if ([intent isKindOfClass:[BreakfastPunchIntent class]]){
BreakfastPunchIntentHandler *intentHandler = [[BreakfastPunchIntentHandler alloc] init];
return intentHandler;
}else if ([intent isKindOfClass:[SportPunchIntent class]]){
SportPunchIntentHandler *intentHandler = [[SportPunchIntentHandler alloc] init];
return intentHandler;
}
return nil;
}
handlerForIntent
方法是整个 Intents Extension 的入口,当 siri 通过语音指令匹配到对于的 Intent , 该方法就会被执行。这里我 return 我创建一个 DailyPunchIntentHandler 类,该类准守DailyPunchIntentHandling
协议。 用来处理匹配到 Intent 后的 UI 显示以及后续操作。
该协议有两个方法:
该方法是在 siri 匹配到相应的 Intent 时候调用。
通过 completion 返回一个 DailyPunchIntentResponse。
- (void)confirmDailyPunch:(DailyPunchIntent *)intent completion:(void (^)(DailyPunchIntentResponse *response))completion NS_SWIFT_NAME(confirm(intent:completion:));
而下面这个方法是用户对 Intent UI 的操作回调,比如用户点击了图一的“是”这个按钮。
- (void)handleDailyPunch:(nonnull DailyPunchIntent *)intent completion:(nonnull void (^)(DailyPunchIntentResponse * _Nonnull))completion;
具体的实现,在-(void)confirmDailyPunch
这个方法里,我的需求是要先判断用户是否登录。
如果登录,由 completion 返回的DailyPunchIntentResponse 的 code 为我最初定义的一个状态 DailyPunchIntentResponseCodeFailureUnLogin;
如果已经登录,则返回 DailyPunchIntentResponseCodeReady,表示一切准备就绪。
代码如下:
if(!self.isLogin){
DailyPunchIntentResponse *intentResponse = [[DailyPunchIntentResponse alloc] initWithCode:DailyPunchIntentResponseCodeFailureUnLogin userActivity:nil];
completion(intentResponse);
}else{
DailyPunchIntentResponse *intentResponse = [[DailyPunchIntentResponse alloc] initWithCode:DailyPunchIntentResponseCodeReady userActivity:nil];
completion(intentResponse);
}
而 - (void)handleDailyPunch
方法,最好也要对未登录做处理,这样当提示请用户先登录app的时候,用户点击“是”, 我们可以传递DailyPunchIntentResponseCodeContinueInApp
, 那么就会自动启动 APP。
如果是登录状态,那么就去向服务器发送打卡请求:
请求成功,传递 DailyPunchIntentResponseCodeSuccess
状态。
请求失败,传递之前自定义的 DailyPunchIntentResponseCodeFailureWithSomething
状态,并且附带上 errorMessage 信息。供后面的 IntentUI使用。
具体如下:
if(self.isLogin){
[[self dailyPunch] subscribeNext:^(id x) {
completion([[DailyPunchIntentResponse alloc] initWithCode:DailyPunchIntentResponseCodeSuccess userActivity:nil]);
} error:^(NSError *error) {
NSString *errorMessage = error.userInfo[@"NSLocalizedDescription"];
DailyPunchIntentResponse *response = [[DailyPunchIntentResponse alloc] initWithCode:DailyPunchIntentResponseCodeFailureWithSomething userActivity:nil];
response.errorMessage = errorMessage;
completion(response);
}];
}else{
completion([[DailyPunchIntentResponse alloc] initWithCode:DailyPunchIntentResponseCodeFailureRequiringAppLaunch userActivity:nil]);
}
Intents Extension UI
最后就是我们的 Intent UI登场了。打开文件夹目录,会发现系统自动创建了一个名为IntentViewController
的类。
该类只有一个方法,很长的方法:
- (void)configureViewForParameters:(NSSet <INParameter *> *)parameters ofInteraction:(INInteraction *)interaction interactiveBehavior:(INUIInteractiveBehavior)interactiveBehavior context:(INUIHostedViewContext)context completion:(void (^)(BOOL success, NSSet <INParameter *> *configuredParameters, CGSize desiredSize))completion;
上面提到的 通过 completion 传递的 DailyPunchIntentResponse,就是传递到该方法。然后通过不同的状态,来展示给用户不同的UI。
需要注意的是,DailyPunchIntentResponse 的 code 如果是系统自动创建的,会和 interaction.intentHandlingStatus 相互对应。
但如果是自定义的状态,他们的 intentHandlingStatus 都对应着 INIntentHandlingStatusSuccess。
先看具体的代码实现:
[[self.view subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];
CGSize desiredSize = CGSizeZero;
if (interaction.intentHandlingStatus == INIntentHandlingStatusReady) {
desiredSize = [self displayPunchContentFrom:interaction.intent];
}else if(interaction.intentHandlingStatus == INIntentHandlingStatusSuccess){
INIntentResponse *response = interaction.intentResponse;
//每日打卡
if ([response isKindOfClass:[DailyPunchIntentResponse class]]) {
DailyPunchIntentResponse *dailyResponse = (DailyPunchIntentResponse *)response;
if (dailyResponse.code == DailyPunchIntentResponseCodeFailureUnLogin) {
desiredSize = [self displayPunchUnLoginResultFrom:interaction.intent];
}else if(dailyResponse.code == DailyPunchIntentResponseCodeFailureWithSomething){
desiredSize = [self displayPunchFailedResult:dailyResponse.errorMessage];
}else{
desiredSize = [self displayPunchSuccessResultFrom:interaction.intent];
}
}
}
if (CGSizeEqualToSize(desiredSize,CGSizeZero)) {
completion(NO, [NSSet new], CGSizeZero);
return;
}else{
if (completion) {
completion(YES, parameters, desiredSize);
}
}
逻辑很清楚,先获取 interaction.intentHandlingStatus 的值:
如果是 raedy状态,就正常创建UI;
否则,获取 interaction 的 intentResponse ,从而拿到我们自定义的状态:
根据对应的 code 去创建不同的UI, 总之,别忘了 addSubView。这里以未登录状态的 UI 为例:
- (CGSize)displayPunchUnLoginResultFrom:(INIntent *)intent{
self.resultView.titleLabel.text = @"请先登录薄荷健康";
self.resultView.topImageView.image = [UIImage imageNamed:@"ic_failed_siri"];
[self.view addSubview:self.resultView];
CGFloat width = 320;
if (@available(iOS 10.0,*)) {
width = self.extensionContext.hostedViewMaximumAllowedSize.width;
}
CGRect frame = CGRectMake(0, 0, width, 110);
self.resultView.frame = frame;
return frame.size;
}
添加语音录制
和上篇博客通过 NSUserActivity的方式类似,唯一的不同就是 INShortcut 初始化方式的不同。
DailyPunchIntent *intent = [[DailyPunchIntent alloc] init];
INShortcut *shortCuts = [[INShortcut alloc] initWithIntent:intent];
INUIAddVoiceShortcutViewController *vc = [[INUIAddVoiceShortcutViewController alloc] initWithShortcut:shortCuts];
vc.delegate = self;
[self presentViewController:vc animated:YES completion:nil];
Donate
每当用户在 app内 有某个行为的时候,你可以选择 Donate ,这样 siri 通过机器学习,智能预测用户未来的行为发生的场景。
只有完成了 Donate ,Siri 才能在正确预测并且出现在屏锁,SportLight 等界面。
DailyPunchIntent *intent = [[DailyPunchIntent alloc] init];
INInteraction *vc = [INInteraction alloc] initWithIntent:intent response:nil];
[interaction donateInteractionWithCompletion:^(NSError * _Nullable error) {
}];
到这里,已经完成了 iOS12 的 Siri ShortCuts 的核心功能开发。有关获取用户登录状态等和 APP 数据共享的需求,可以参考 App Extension 与 App 之间的数据共享 这篇文章。
参考资料:
苹果官方WWDC2018视频
苹果官方 Siri ShortCuts Demo