我的demo(內(nèi)含詳細(xì)注解):https://github.com/huchuankan/IOSDemo
ios14補(bǔ)充,ios的Siri呼喚出來(lái)從全屏變成了 home鍵上面的彩色圈圈,這種模式下好像無(wú)法用快捷命令的短語(yǔ)打開(kāi)界面, 我的方式是 設(shè)置--siri 里面打開(kāi)“鍵入以使用Siri”按鈕, 這樣在呼喚 ”嘿 siri“的時(shí)候,還是全屏的siri模式,這時(shí)候說(shuō)出短語(yǔ),就能實(shí)現(xiàn)功能, 個(gè)人琢磨不知道為啥,也不知道有沒(méi)有其他辦法
原文有一點(diǎn)小坑,下文為我demo后的改進(jìn),可對(duì)比閱讀
今天這篇文章就來(lái)介紹另外一種功能,通過(guò) Intents Extension 實(shí)現(xiàn)不打開(kāi)app 去完成某個(gè)任務(wù)。先來(lái)看下效果:
SiriKit Intent Definition File
在新建Extension之前,我們要通過(guò) file->newfile, 選擇 SiriKit Intent Definition File。創(chuàng)建好后是這個(gè)樣子的:
上圖中,我一共自定義了三個(gè) Intent, 分別是 DailyPunch、BreakfastPunch和 SportPunch。在這個(gè)界面,你可以設(shè)置 Intent 的 category,title和一些參數(shù)。每個(gè) Intent 都對(duì)應(yīng)一個(gè) Response, 如下圖所示。你可以在response定義參數(shù)和錯(cuò)誤類型。如圖所示,我定義了 errorMessage 的參數(shù) 和 failureUnLogin等兩個(gè)錯(cuò)誤類型。這里 errorMessage 用來(lái)傳遞服務(wù)器返回的錯(cuò)誤信息。
注意,需要交互按鈕的就勾選 User confirmation required
創(chuàng)建完成后,編譯一下項(xiàng)目,xcode 會(huì)自動(dòng)生成對(duì)應(yīng)的類,我這里的話會(huì)生成 DailyPunchIntent 等三個(gè)類,每個(gè)類包含了 DailyPunchIntentHandling 協(xié)議和 DailyPunchIntentResponse 類等所需要的內(nèi)容。
需要注意的是,這些類不會(huì)出現(xiàn)在項(xiàng)目的目錄中,有點(diǎn)和 Core Data 類似。
但你可以正常使用,可以為其新建 Category 或者導(dǎo)入頭文件就可以直接使用。
注意,自定義code請(qǐng)都選success ,不然就是自定義錯(cuò)誤code了,系統(tǒng)已經(jīng)有failure這個(gè)code了
我這里通過(guò) Category 為每個(gè) Intent 類添加了 suggestedInvocationPhrase 屬性,可以在用戶錄制的時(shí)候給出建議短語(yǔ)。
@implementation DailyPunchIntent (PXDailyPunch)
- (instancetype)init{
? ? self = [super init];
? ? if (self) {
? ? ? ? self.suggestedInvocationPhrase = @"打卡";
? ? }
? ? return self;
}
@end
Intents Extension
接下來(lái)就是創(chuàng)建 Extension 了。通過(guò) file -> new -> target , 選擇 Intents Extension 即可。為了讓 Extension 的界面便于控制,我選擇了 Include UI Extension。這樣就同時(shí)創(chuàng)建了兩個(gè)Extension。
Intents Extension 創(chuàng)建好后,會(huì)自動(dòng)出現(xiàn)一個(gè)名為 IntentHandler 的類。對(duì)于非即時(shí)通訊類的需求,可以刪除其他的方法,保留下面一個(gè)方法即可:
- (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?方法是整個(gè) Intents Extension 的入口,當(dāng) siri 通過(guò)語(yǔ)音指令匹配到對(duì)于的 Intent , 該方法就會(huì)被執(zhí)行。這里我 return 我創(chuàng)建一個(gè) DailyPunchIntentHandler 類,該類準(zhǔn)守DailyPunchIntentHandling協(xié)議。 用來(lái)處理匹配到 Intent 后的 UI 顯示以及后續(xù)操作。
該協(xié)議有兩個(gè)方法:
以下2個(gè)方法的調(diào)用順序:intetnt識(shí)別后,執(zhí)行1--> 打開(kāi)siriUI的界面-->(如果有交互按鈕,等點(diǎn)擊按鈕后再繼續(xù),如果沒(méi)有就繼續(xù))-->執(zhí)行2-->再刷新siriUI的界面
1.該方法是在 siri 匹配到相應(yīng)的 Intent 時(shí)候調(diào)用。
通過(guò) completion 返回一個(gè) DailyPunchIntentResponse。
- (void)confirmDailyPunch:(DailyPunchIntent *)intent completion:(void (^)(DailyPunchIntentResponse *response))completion NS_SWIFT_NAME(confirm(intent:completion:));
2.而下面這個(gè)方法是用戶對(duì) Intent UI 的操作回調(diào),比如用戶點(diǎn)擊了圖一的“是”這個(gè)按鈕。
- (void)handleDailyPunch:(nonnull DailyPunchIntent *)intent completion:(nonnull void (^)(DailyPunchIntentResponse * _Nonnull))completion;
具體的實(shí)現(xiàn),在-(void)confirmDailyPunch這個(gè)方法里,我的需求是要先判斷用戶是否登錄。
如果登錄,由 completion 返回的DailyPunchIntentResponse 的 code 為我最初定義的一個(gè)狀態(tài) DailyPunchIntentResponseCodeFailureUnLogin;
如果已經(jīng)登錄,則返回 DailyPunchIntentResponseCodeReady,表示一切準(zhǔn)備就緒。
代碼如下:
if(!self.isLogin){
DailyPunchIntentResponse *intentResponse = [[DailyPunchIntentResponse alloc] initWithCode:DailyPunchIntentResponseCodeFailureUnLogin userActivity:nil];
completion(intentResponse);?
}else{
DailyPunchIntentResponse *intentResponse = [[DailyPunchIntentResponse alloc] initWithCode:DailyPunchIntentResponseCodeReady userActivity:nil];
completion(intentResponse);?
}
注意:經(jīng)測(cè)試上面方法建議傳initWithCode:DailyPunchIntentResponseCodeReady和initWithCode:DailyPunchIntentResponseCodeSuccess這2個(gè)code, 以為自定義code傳過(guò)去后再siriUI處獲取的枚舉值都是0
而?- (void)handleDailyPunch方法,最好也要對(duì)未登錄做處理,這樣當(dāng)提示請(qǐng)用戶先登錄app的時(shí)候,用戶點(diǎn)擊“是”, 我們可以傳遞DailyPunchIntentResponseCodeContinueInApp, 那么就會(huì)自動(dòng)啟動(dòng) APP。
如果是登錄狀態(tài),那么就去向服務(wù)器發(fā)送打卡請(qǐng)求:
請(qǐng)求成功,傳遞?DailyPunchIntentResponseCodeSuccess狀態(tài)。
請(qǐng)求失敗,傳遞之前自定義的?DailyPunchIntentResponseCodeFailureWithSomething?狀態(tài),并且附帶上 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]);
}
注意在;上面方法的自定義code能在siriUId response.code中獲取到
Intents Extension UI
最后就是我們的 Intent UI登場(chǎng)了。打開(kāi)文件夾目錄,會(huì)發(fā)現(xiàn)系統(tǒng)自動(dòng)創(chuàng)建了一個(gè)名為IntentViewController?的類。
該類只有一個(gè)方法,很長(zhǎng)的方法:
- (void)configureViewForParameters:(NSSet <INParameter *> *)parameters ofInteraction:(INInteraction *)interaction interactiveBehavior:(INUIInteractiveBehavior)interactiveBehavior context:(INUIHostedViewContext)context completion:(void (^)(BOOL success, NSSet <INParameter *> *configuredParameters, CGSize desiredSize))completion;
1
上面提到的 通過(guò) completion 傳遞的 DailyPunchIntentResponse,就是傳遞到該方法。然后通過(guò)不同的狀態(tài),來(lái)展示給用戶不同的UI。
需要注意的是,DailyPunchIntentResponse 的 code 如果是系統(tǒng)自動(dòng)創(chuàng)建的,會(huì)和 interaction.intentHandlingStatus 相互對(duì)應(yīng)。
但如果是自定義的狀態(tài),他們的 intentHandlingStatus 都對(duì)應(yīng)著 INIntentHandlingStatusSuccess。
先看具體的代碼實(shí)現(xiàn):
? ? [[self.view subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];
? ? CGSize desiredSize = CGSizeZero;
? ? if (interaction.intentHandlingStatus == INIntentHandlingStatusReady) {
? ? ? ? desiredSize = [self displayPunchContentFrom:interaction.intent];
? ? }else if(interaction.intentHandlingStatus == ConnectorDemoIntentResponseCodeInProgress){
? ? ? ? INIntentResponse *response = interaction.intentResponse;
? ? ? ? //每日打卡
? ? ? if ([response isKindOfClass:[DailyPunchIntentResponse class]]) {
? ? ? ? ? ? DailyPunchIntentResponse *dailyResponse = (DailyPunchIntentResponse *)response;
? if (dailyResponse.code == ConnectorDemoIntentResponseCodeSuccess) {?
} else? 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狀態(tài),就正常創(chuàng)建UI;
否則,獲取 interaction 的 intentResponse ,從而拿到我們自定義的狀態(tài):
根據(jù)對(duì)應(yīng)的 code 去創(chuàng)建不同的UI, 總之,別忘了 addSubView。這里以未登錄狀態(tài)的 UI 為例:
- (CGSize)displayPunchUnLoginResultFrom:(INIntent *)intent{
? ? self.resultView.titleLabel.text = @"請(qǐng)先登錄薄荷健康";
? ? 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;
}
添加語(yǔ)音錄制
和上篇博客通過(guò) 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
每當(dāng)用戶在 app內(nèi) 有某個(gè)行為的時(shí)候,你可以選擇 Donate ,這樣 siri 通過(guò)機(jī)器學(xué)習(xí),智能預(yù)測(cè)用戶未來(lái)的行為發(fā)生的場(chǎng)景。
只有完成了 Donate ,Siri 才能在正確預(yù)測(cè)并且出現(xiàn)在屏鎖,SportLight 等界面。
DailyPunchIntent *intent = [[DailyPunchIntent alloc] init];
INInteraction *vc = [INInteraction alloc] initWithIntent:intent response:nil];
[interaction donateInteractionWithCompletion:^(NSError * _Nullable error) {
}];
通過(guò)Intents Extension UI喚起App
3、在AppDelegate中處理Siri打開(kāi)APP請(qǐng)求 (Handle Shortcut)
通過(guò)userActivity的type值判斷是否為Siri Shortcuts呼起,做相應(yīng)的邏輯處理。
-(BOOL)application:(UIApplication*)application continueUserActivity:(NSUserActivity*)userActivity restorationHandler:(void(^)(NSArray<id<UIUserActivityRestoring>>*_Nullable))restorationHandler{
NSLog(@"continueUserActivity");
if([userActivity.activityType isEqualToString:@"loying.LearnSiriShortcut.type"]){
// 做自己的業(yè)務(wù)邏輯
}
return YES;
}
到這里,已經(jīng)完成了 iOS12 的 Siri ShortCuts 的核心功能開(kāi)發(fā)。有關(guān)獲取用戶登錄狀態(tài)等和 APP 數(shù)據(jù)共享的需求,可以參考App Extension 與 App 之間的數(shù)據(jù)共享這篇文章。
參考資料:
蘋(píng)果官方WWDC2018視頻
蘋(píng)果官方 Siri ShortCuts Demo
參考鏈接 :https://blog.csdn.net/u013749108/article/details/81413817