撩起Siri

iOS 10 来了,Siri也有了大变化,可以让Siri帮我们发消息,开始锻炼,具体有如下六类服务:

  • 语音和视频通话
  • 发送消息
  • 收款或者付款
  • 图片搜索
  • 管理锻炼
  • 行程预约

下面开始看看怎么撩Siri

创建 Intents Extension

SiriKit的互动是通过Intents Extension实现的,我们需要创建一个Intents Extension来与Siri互动并响应操作,所以,在此之前你必须有一个app,或者新建一个项目

为已有项目添加Intents Extension,有以下几点:

  • 把项目CapabilitySiri启用;见Xcode项目设置
  • 在项目中添加一个Intents Extension,并且在Info.plist中进行配置;见Xcode项目设置
  • 请求Siri权限;见请求Siri权限
  • 定义一个处理intents的对象;见处理Intents
  • 定义一些自定义的词汇;见自定义词汇

    注:
    你可以在项目中添加一个Intents UI extension来自定义Siri或者地图的显示界面,Intents UI extension不能代替Intents Extension,两者是不同的东西,有关Intents UI Extension,见创建Intents UI Extension

Xcode项目设置

除了添加一个Intents extension target到你的项目中,你还需要做一些设置

要使用SiriKit,你必须启用Siri capability, 就像启用iCloudpush notifications(通知),in-app purchase(应用内购买),让App Store知道你的app支持Siri

在app中启用Siri功能

  1. 在Xcode中打开项目
  2. 在项目设置中,选中app的target
  3. 切换到Capabilities这一项
  4. 找到Siri这一项,启用


在项目中新增一个Intents extension target

  1. 在Xcode中打开项目
  2. 选择 File > New > Target
  3. 选择 Intents extension
  4. 点击下一步
  5. 起一个名字,和工程名差不多就好了,如果你打算自定义Siri的UI界面,把Include UI Extension钩上
  6. 点击完成

    注:
    你可以添加多个 Intents extension,但每个 Intents extension必须是支持不同的Intents,除非是有性能优势或者减少了内存占用,否则不建议这么做




指定扩展(extension)支持的Intents

  1. 在Xcode中,选择新创建的 Intents extension中的Info.plist文件
  2. 点击展开NSExtension,然后展开NSExtensionAttributes,可以看到有IntentsSupportedIntentsRestrictedWhileLocked 这两个键
  3. IntentsSupported下,每一个字符串表示的是应用扩展处理的Intent事件的类名,我们需要为每个扩展都添加一行(这个必须设置)
  4. 如果需要在锁屏时禁用某个功能是锁屏情况下,则再在IntentsRestrictedWhileLocked中加入相应项的Intent这个为可选


当用户说出了一些语义相似的话,也许会启用多个扩展,这时候,会根据在IntentsSupported下面的数组的顺序决定哪个扩展先响应,所以,当你有一个Intents非常重要需要先响应,你需要将它在数组中靠前排列

请求Siri权限

用户必须授予应用程序的权限使用SiriKit。要请求您的应用程序的权限,如下:

  • 在项目的Info.plist加入一行,键为NSSiriUsageDescription,值为请求访问Siri权限时,弹出框的文字,例如:允许app访问Siri以完成更多操作
  • 调用INPreferences类的requestSiriAuthorization:方法请求授权

当你第一次调用 requestSiriAuthorization的时候,系统将弹出一个请求授权的对话框让用户授权你的app,对话框的描述内容就是你在Info.plist文件中的NSSiriUsageDescription对应的值,用户可以批准或者拒绝你的请求授权,随后也可以在系统设置里面修改授权状态,当你再次调用requestSiriAuthorization时,系统会记住用户之前的授权状态而不会再次提示用户进行授权

测试extension

先把我们自己的app跑起来,然后运行扩展(extension),会看到如下窗口:




选择我们自己的app,开始run,在run起来的程序中请求授权:






处理Intents

当我们实现了Intents extension扩展并产生了一个Siri请求事件时,一个典型的Intent事件的处理过程中总共有这三个步骤ResolveConfirmHandle

  • Resolve阶段。在Siri获取到用户的语音输入之后,生成一个INIntent对象,将语音中的关键信息提取出来并且填充对应的属性。这个对象在稍后会传递给我们设置好的INExtension子类对象进行处理,根据子类遵循的不同服务protocol来选择不同的解决方案
  • Confirm阶段。在上一个阶段通过handler(for intent:)返回了处理intent的对象,此阶段会依次调用confirm打头的实例方法来判断Siri填充的信息是否完成。匹配的判断结果包括Exactly one match、Two or more matches以及No match三种情况。这个过程中可以让Siri向用户征求更具体的参数信息
  • 在confirm方法执行完成之后,Siri进行最后的处理阶段,生成答复对象,并且向此intent对象确认处理结果然后执显示结果给用户看


Resolve

这个阶段需要我们找到消息的具体接收者。在这个过程中,可能会出现三种情况:Exactly one match、Two or more matches以及No matches,对于这三种情况的处理,代码如下:

func resolveRecipients(forSendMessage intent: INSendMessageIntent, with completion: @escaping ([INPersonResolutionResult]) -> Void) {

    if let recipients = intent.recipients {

        // If no recipients were provided we'll need to prompt for a value.
        if recipients.count == 0 {
            completion([INPersonResolutionResult.needsValue()])
            return
        }

        var resolutionResults = [INPersonResolutionResult]()
        for recipient in recipients {

            var matchingContacts : [INPerson] = []
            //1
            contacts.forEach({ (contact) in
                if contact.hasPrefix(recipient.displayName) {

                    let handle = INPersonHandle.init(value: contact + "test", type:.unknown)

                    matchingContacts.append(INPerson.init(personHandle: handle, nameComponents:nil, displayName: contact, image: nil, contactIdentifier: nil, customIdentifier: nil))
                }
            })

            switch matchingContacts.count {
            case 2  ... Int.max:
                // We need Siri's help to ask user to pick one from the matches.
                resolutionResults += [INPersonResolutionResult.disambiguation(with: matchingContacts)]

            case 1:
                // We have exactly one matching contact
                resolutionResults += [INPersonResolutionResult.success(with: recipient)]

            case 0:
                // We have no contacts matching the description provided
                resolutionResults += [INPersonResolutionResult.unsupported()]

            default:
                break

            }
        }
        completion(resolutionResults)
    }

}

1.此处是程序根据Siri提供的接收人信息,筛选出相应的收信人。例如:我们的通讯录有陈经理陈小哥胖子,我们对Siri说,用小马(我们的app名)发消息给姓陈的,因为此处有两位姓陈的,所以我们需要将两个都匹配出来,放入matchingContacts中,使用INPersonResolutionResult.disambiguation,让Siri询问用户具体是哪一位姓陈的

下面是匹配消息内容:

func resolveContent(forSendMessage intent: INSendMessageIntent, with completion: @escaping (INStringResolutionResult) -> Void) {
    if let text = intent.content, !text.isEmpty {
        completion(INStringResolutionResult.success(with: text))
    } else {
        completion(INStringResolutionResult.needsValue())
    }
}

Confirm

在该阶段,我们检测用户的登录状态,以决定是否发送消息,在这个阶段,你可以最后一次修改Intent内容,未登录时,我们传入failureRequiringAppLaunch启动app进行相应的登录操作:

func confirm(sendMessage intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Void) {

    if hasLogin() {
        completion(INSendMessageIntentResponse(code: .success, userActivity: nil))
    }else{

        // Creating our own user activity to include error information.
        let userActivity = NSUserActivity(activityType: String(describing: INSendMessageIntent.self))
        userActivity.userInfo = [NSString(string: "error"):NSString(string: "UserLoggedOut")]

        completion(INSendMessageIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity))
    }
}

Handle

到了这个阶段,检查下各项信息是否为空,调用api去发送消息,有必要的时候保存数据,然后反馈任务状态给Siri

 func handle(sendMessage intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Void) {

    if intent.recipients != nil && intent.content != nil {
        // Send the message.
        let success = sendMessage(content: intent.content!,contacts: intent.recipients!)
        completion(INSendMessageIntentResponse(code: success ? .success : .failure, userActivity: nil))
    }
    else {
        completion(INSendMessageIntentResponse(code: .failure, userActivity: nil))
    }
}  

运行之后:



创建Intents UI Extension

上一步创建Intents Extension的时候我们已经创建了Intents UI Extension了,这里,我们可以改变展示的ui,在Storyboard里面加入一个label



运行:



Demo见此

参考文章:
SiriKit Programming Guide
iOS开发——SiriKit应用
SiriKit 初探 —— WWDC 2016 技术赏析