最近公司的项目中需要加入Siri 快捷方式的功能,引入过程中遇到很多问题特此记录。
需求
在系统应用“快捷指令”中添加指令,无须打开原App即可调用运行功能。
Flutter 部分
我的是Flutter工程,native同学可以直接跳过这部分哈
主要是使用methodChannel做通信转发事件,安卓和iOS需要各自声明。
以下文为例:
Future<Map> methodChannelGainShortcutsList() async {
///声明通信频道
const MethodChannel methodChannel =
MethodChannel('com.xxxx.xxx.xxxxxx/runner');
///声明事件
Map result = await methodChannel.invokeMethod('gainXXXXXX');
return result;
}
iOS需要在AppDelegate中添加:
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
//meth
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let channel = FlutterMethodChannel(name: "com.xxxx.xxx.xxxxxx/runner", binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
if call.method == "gainXXXXXX" {
///dosthing
result(["key":""]);//return内容根据自己需求来
}
else {
result(FlutterMethodNotImplemented)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
iOS部分
接下来开始Siri Shortcuts的项目配置,第一步是添加Siri Kit Intent的配置文件。
右下角加号->File
找到SiriKit Intent,Next
右下角加号,新增一个 Intent
创建后是这个样子,会自动生成一个Response
Parameter里可以做很多文章,可以引用多种类型,自建Enum等等
Command+B build一下,intent会自动生成对应的类文件。
//
// IntentIntent.swift
//
// This file was automatically generated and should not be edited.
//
#if canImport(Intents)
import Intents
@available(iOS 12.0, macOS 11.0, watchOS 5.0, *) @available(tvOS, unavailable)
@objc(IntentIntent)
public class IntentIntent: INIntent {
}
@available(iOS 12.0, macOS 11.0, watchOS 5.0, *) @available(tvOS, unavailable)
@objc(IntentIntentHandling)
public protocol IntentIntentHandling: NSObjectProtocol {
@available(*, renamed: "handle(intent:)")
@objc(handleIntent:completion:)
func handle(intent: IntentIntent, completion: @escaping (IntentIntentResponse) -> Swift.Void)
后面省略...
需要注意的是,有时Intents编译缓慢,或者不同Xcode版本下,会有未编译的情况出现。
如下图右侧中,如果Custom Class的右侧没有出现箭头,则说明文件未编译成功,需要多编译尝试几次。
成功时,如下图会有小箭头。
如果希望更改类名,同样是修改这里Custom Class的值,然后编译。
接下来在General中添加Targets
搜索intents,并添加
记住勾选Include UI Extension帮你自动创建对应的UITarget,点击Finish
注意
需要将前面创建的Intents.intentdefinition文件在新建的Targets中引入(将红框内勾选中)。
这个时候编译一下,会出现一个编译循环问题:
编译器提示:
Cycle inside Runner; building could produce unreliable results.
应该是在Flutter中才会出现。是由于编译顺序导致的循环依赖问题。
解决方法也很简单
找到主工程 -> Build Phases ->调整顺序
主要是下图红框内,错序会出现循环依赖问题
亲测按照本图内顺序调整,即可通过编译
注意
如果IntentsSupported中没有对应Intents类名,siriex会在快捷方式中找不到不到对应的处理函数。所以要确保类名已添加,如下图。
同时,这里会自动生成IntentHandler的类及.h.m文件,IntentHandler是所有Intents的处理函数的入口。
所以在NSExtensionPrincipalClass中的值也必须保留,更名时必须修改。
这里的INSendMessageIntent、INSearchForMessagesIntent、INSetMessageAttributeIntent,也会自动生成,如果功能用不上,对应代码也可以删除(本例用不到故删除)
清理干净后IntentHandler.m文件如下:
//
// IntentHandler.m
//
#import "IntentHandler.h"
#import "XXHandler.h"
#import "XXXHander.h"
@interface IntentHandler ()
@end
@implementation IntentHandler
- (id)handlerForIntent:(INIntent *)intent {
if([intent isKindOfClass:XXIntent.class]){
return [[XXIntentHandler alloc]init];
}
if([intent isKindOfClass:XXXIntent.class]){
return [[XXXIntentHander alloc]init];
}
return nil;
}
在handlerForIntent中进行类型分发
而在对应的XXXIntentHandler中,则需要重写handleXXX与confirmXXX两个函数,这里方法名是自动生成的。
//
// XXXIntentHandler.m
//
#import "XXXIntentHandler.h"
#import "XXXIntent.h"
@interface XXXIntentHandler()<XXXIntentHandling>
@end
@implementation XXXIntentHandler
- (void)handleXXX:(XXXIntent *)intent completion:(void (^)(XXXIntentResponse * _Nonnull))completion{
completion( [[XXXIntentResponse alloc] initWithCode:XXXIntentResponseCodeSuccess
userActivity:nil]);
}
- (void)confirmXXX:(XXXIntent *)intent completion:(void (^)(XXXIntentResponse * _Nonnull))completion{
completion([[XXXIntentResponse alloc] initWithCode:XXXIntentResponseCodeSuccess
userActivity:nil]);
}
@end
到此就创建完成了,可以进行业务方面的工作。
快捷方式的UI交互需要在siriui中自定义。
其他问题
Bundle Identifier
你的主工程和siriex、siriexUI的Bundle Identifier必须符合关联递进关系。
举个例子:
主工程: com.org.projectname
siriex: com.org.projectname.siriex
siriexui: com.org.projectname.siriexui
如果是修改了Bundle Identifier或者调试工程的时候需要用其他Bundle Identifier也需要修改对应是siriex、siriexui的Bundle Identifier。(App Group同理)
<NSUserActivity> has an interaction attached but it is not handled
在过去的iOS版本中是会有报错提示,在更高版本中某些情况也会有这个报错。
在最初通常是由于找不到handlerForIntent对应实现。
解决方法也很简单,只要在handlerForIntent中return了对应类并实现了handleXXX方法就行。
为什么我的Siri Shortcuts 直接打开了应用程序?不在外部执行?
情况一:
在某些情况下,会造成这个问题,同样也是游这个NSUserActivity类造成的错误。比如使用了Flutter第三方库 flutter_siri_shortcuts和 siri_shortcuts。
这两个库提供的方法,是基于以NSUserActivity实现的,所以他们实现的功能是通过siri shortcuts 快速打开应用内部实现,所以使用了这些库会有这个情况出现。
情况二:
通常在业务逻辑中我们需要创建INShortcut来
INShortcut *shortcut = [[INShortcut alloc] initWithIntent:intent];
INUIAddVoiceShortcutViewController *voiceShortcutVC = [[INUIAddVoiceShortcutViewController alloc] initWithShortcut:shortcut];
voiceShortcutVC.delegate = self;
[self presentViewController:voiceShortcutVC animated:YES completion:nil];
这里一定要使用initWithIntent方法创建,如果使用了initWithUserActivity方法创建则会出现这个问题。
在底层中,Intent、UserActivity应该是继承关系,如果intent没有实现会自动进入UserActivity相关的逻辑。
情况三:
由于快捷指令在数据安全、高效、扩展性强个方面的优势。Apple在Siri方面对于Shortcuts功能也进行了多次迭代与扩展,导致了Siri Shortcuts也有着不同版本的支持。
在Xcode15.3的版本中添加intents会默认Minimum Deployments为最高版本17.4(真是让人相当无语)
当系统版本低于Minimum Deployments运行该快捷指令时,也会出现直接跳转的问题。
这种情况,很容易在外部分享的快捷指令中出现、在老项目新增Intents扩展的过程中出现、在已有intents扩展但是Xcode升级后出现。