# iOS10新特性之SiriKit

语言：zh-CN
原文：[iOS10新特性之SiriKit](<https://allluckly.cn/%E6%8A%95%E7%A8%BF/tuogao50>)
中文：[中文 HTML](<https://allluckly.cn/%E6%8A%95%E7%A8%BF/tuogao50>)
English: [English HTML](<https://allluckly.cn/en/journal/2016-06-19-what-s-new-in-ios-10-sirikit/>)
发布于: 2016-06-19T00:00:00+08:00
内容更新于: 2016-06-19T00:00:00+08:00
署名：Sindri

介绍 WWDC 2016 后的 SiriKit 接入，从创建 Intents Extension 到实现意图处理和事件界面，说明语音请求如何被解析并交给应用扩展提供相应内容。

原文署名Sindri，Bison 编辑收录。

[原文所列来源](<http://www.jianshu.com/p/0881bb0ff538>)

**阅读前** 这是 2015–2017 年间的技术记录，本次整理未重新运行验证示例。原文明确面向 iOS 10 与 Xcode 8，保留当时 SiriKit 的服务类型和扩展配置。

在6月14日凌晨的`WWDC2016`大会上，苹果提出`iOS10`是一次里程碑并且推出了十个新特性，大部分的特性是基于`iPhone`自身的原生应用的更新，具体的特性笔者不在这里再次叙述，请看客们移步[WWDC2016下载](<https://developer.apple.com/ios/download/>)自行观赏。要说里程碑在笔者看来有些夸大其实了，不过新增的通知中心联动`3D Touch`确实为人机交互带来新的发展，另外一个最大的亮点在于`Siri`的接口开放。在`iOS10`中提供了`SiriKit`框架在用户使用`Siri`的时候会生成`INExtension`对象来告知我们的应用，通过实现方法来让`Siri`获取应用想要展示给用户的内容

编辑：[Bison](<https://allluckly.cn/>)  
 投稿：[Sindri的小巢](<http://www.jianshu.com/p/0881bb0ff538>)

![1](<https://allluckly.cn/images/blog/tuogao/tougao50/1.jpg>)

在`iOS10`之后，苹果希望`Siri`能够给用户带来更多的功能体验，基于这个出发点，新增了`SiriKit`框架。`Siri`通过语言处理系统对用户发出的对话请求进行解析之后生成一个用来描述对话内容的`Intents`事件，然后通过`SiriKit`框架分发给集成框架的应用程序以此来获取应用的内容，比如完成类似`通过文字匹配查找应用聊天记录、聊天对象`的功能，此外它还支持`为用户使用苹果地图时提供应用内置服务`等功能。通过官方文档我们可以看到`SiriKit`框架支持的六类服务分别是：

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

`Siri`和`Maps`通过`Intents extension`的扩展方式和我们的应用进行交互，其中，类型为`INExtension`的对象扮演着`Intents extension`扩展中直接协同`Siri`对象共同响应用户请求的关键角色。当我们实现了`Intents extension`扩展并产生了一个`Siri`请求事件时，一个典型的`Intent`事件的处理过程中总共有这三个步骤`Resolve`、`Confirm`和`Handle`：

- `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`对象确认处理结果然后执显示结果给用户看

![1](<https://allluckly.cn/images/blog/tuogao/tougao50/2.jpg>)

具体的执行过程请参考[文档](<https://developer.apple.com/library/prerelease/content/documentation/Intents/Conceptual/SiriIntegrationGuide/ResolvingandHandlingIntents.html#//apple_ref/doc/uid/TP40016875-CH5-SW1>)和[讲解视频](<https://developer.apple.com/videos/play/wwdc2016/225/>)

## 创建Intents Extension

`SiriKit`通过添加`App Extension`的方式来完成集成，这是一种独立于应用本身运行的代码结构，作为应用的扩展功能，只有在需要的时候系统会唤醒这些`Extension`代码来执行任务，然后在执行完毕之后将其杀死。另一方面，这些`Extension`在运行过程中的可占用内存是较少的，并且由于调用时机的限制，我们也无法在运行期间做一些`坏事`

![1](<https://allluckly.cn/images/blog/tuogao/tougao50/3.jpg>)

现阶段集成`SiriKit`的条件是需要将开发工具升级到`Xcode8`，需要使用开发者账号到官方网站去下载`Xcode8_beta`版，并且需要将一台测试设备升级到`iOS10`系统。选中我们的应用，进入项目总览界面，新增一个`TARGET`

![1](<https://allluckly.cn/images/blog/tuogao/tougao50/4.jpg>)

![1](<https://allluckly.cn/images/blog/tuogao/tougao50/5.jpg>)

如上图所示，我创建的`Intents Extension`被我命名为`LXDSiriExtension`。记住在创建好一个`Extension`的时候，会询问你是否激活这个扩展，勾选是。另外还会提示你是否连同`Intents UI Extension`一并创建了，我们同样选是。这样我们在项目下面总共创建了`LXDSiriExtension`和`LXDSiriExtensionUI`两个`TARGET`，这两个文件目录下面分别存在着一个新的`info.plist`文件，这个文件用来设置`intent`事件发生时我们设置的处理类。这里借用`WWDC`在讲解时的一张`ppt`来了解：

![1](<https://allluckly.cn/images/blog/tuogao/tougao50/6.jpg>)

按图中的层次展开，`IntentsSupported`和`IntentsRestrictedWhileLocked`分别是两个字符串数组，每一个字符串表示的是应用扩展处理的`intent`事件的类名。前者表示支持的事件类型，后者表示在非锁屏状态下执行的事件类型。文件默认是`workout`类型的事件，在这里笔者改成了发送消息`INSendMessageIntent`。除此之外，`NSExtensionPrincipalClass`对应的是`INExtension`子类类名，这个类用来获取处理`intent`事件的类。

![1](<https://allluckly.cn/images/blog/tuogao/tougao50/7.jpg>)

另外，官方讲解中提到了`Embedded frameworks`，在`session`中苹果开发人员通过一个消息聊天应用来示例集成`SiriKit`。由于应用扩展自身的运行机制和应用本身的运行机制不同，彼此之间创建的类是不能访问使用的。因此把我们需要的类开发成`frameworks`的方式导入我们的应用之后就能够在两种之中都使用到这些类。本文未使用`frameworks`导入功能，而是模拟了一个类用来管理事件处理过程中的部分逻辑，但是`Embedded frameworks`这个使用的准则需要记住。这个模拟类的具体代码如下：

```
import Intents

class LXDMatch {
    var handle: String?
    var displayName: String?
    var contactIdentifier: String?

    convenience init(handle: String, _ displayName: String, _ contactIdentifier: String) {
        self.init()
        self.handle = handle
        self.displayName = displayName
        self.contactIdentifier = contactIdentifier
    }

    func inPerson() -> INPerson {
        return INPerson(handle: handle!, displayName: displayName, contactIdentifier: contactIdentifier)
    }
}

class LXDAccount {
    private static let instance = LXDAccount()

    private init() {
        print("only call share() to get an instance of LXDAccount")
    }

    class func share() -> LXDAccount {
        return LXDAccount.instance
    }

    func contact(matchingName: String) -> [LXDMatch] {
        return [LXDMatch(handle: NSStringFromClass(LXDSendMessageIntentHandler.classForCoder()), matchingName, matchingName)]
    }

    func send(message: String, to recipients: [INPerson]) -> INSendMessageIntentResponseCode {
        print("Send a message: \"\(message)\" to \(recipients)")
        return .success
    }
}
```

在完成这些需要的工作之后，我们还需要对应用本身的`Info.plist`配置文件进行设置，新增一个关键字为`NSSiriUsageDescription`的字符串对象，对应填写的字符串将在我们征询用户`Siri`权限的时候显示给用户看。比如`Siri想要访问您的应用信息`之类的提示语。然后通过`INPreferences`类方法向用户请求`Siri`访问权限

```
import Intents

INPreferences.requestSiriAuthorization {
    switch $0 {
    case .authorized:
        print("用户已授权")
        break

    case .notDetermined:
        print("未决定")
        break

    case .restricted:
        print("权限受限制")
        break

    case .denied:
        print("拒绝授权")
        break
    }
}
```

## 代码实现

首先我们需要一个`INExtension`的子类，你也可以在默认创建的子类中实现代码。在方法中，我们通过判断`intent`的类型来创建对应的处理者实例，然后返回。在本文的示例中，假设我们对`Siri`说出这么一句话 `Siri，在微信上告诉我的家人们今天我不回去吃饭了`：

```
class LXDIntentHandler: INExtension {
    override func handler(for intent: INIntent) -> AnyObject? {

        if intent is INSendMessageIntent {
            return LXDSendMessageIntentHandler()
        }
        //  这里可以判断更多类型来返回
        return nil
    }
}
```

通过判断`intent`事件是发送消息的聊天事件后，笔者创建了一个用来处理事件的`LXDSendMessageIntentHandler`类对象，并且返回。在对象创建完成之后需要完成`Resolve`、`Confirm`和`Handle`三个步骤，具体操作需要子类遵循实现`INSendMessageIntentHandling`协议来完成：

- Resolve阶段

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

  ```
  func resolveRecipients(forSendMessage intent: INSendMessageIntent, with completion: ([INPersonResolutionResult]) -> Void) {
        if let recipients = intent.recipients {
            var resolutionResults = [INPersonResolutionResult]()
            for  recipient in recipients {
                let matches = LXDAccount.share().contact(matchingName: recipient.displayName)
                switch matches.count {
                case 2...Int.max:    //两个或更多匹配结果
                    let disambiguations = matches.map { $0.inPerson() }
                    resolutionResults.append(INPersonResolutionResult.disambiguation(with: disambiguations))
                break
  
                case 1:  //一个匹配结果
                    let recipient = matches[0].inPerson()
                resolutionResults.append(INPersonResolutionResult.success(with: recipient))
                    break
  
                case 0:  //无匹配结果
                    resolutionResults.append(INPersonResolutionResult.unsupported(with: .none))
                    break
  
                default:
                    break
                }
            }
            completion(resolutionResults)
        } else {
            //未从用户语音中提取到信息，需要向用户征询更多关键信息
            completion([INPersonResolutionResult.needsValue()])
        }
    }
  ```

  上面的代码用来确认出消息中的`我的家人们`指代的是哪些人，其中每个联系人最终用一个`INPerson`的对象来表示。接着我们需要匹配消息的内容：

  ```
  func resolveContent(forSendMessage intent: INSendMessageIntent, with completion: (INStringResolutionResult) -> Void) {
      if let text = intent.content where !text.isEmpty {
          completion(INStringResolutionResult.success(with: text))
      } else {
          //向用户征询发送的消息内容
          completion(INStringResolutionResult.needsValue())
      }
  }
  ```

  在匹配完消息接收者跟消息内容之后，对于`intent`事件的处理就会进入第二阶段`Confirm`确认值是否正确
- Confirm阶段

  在这个阶段`intent`对象本身的信息预计是已经完成填充的，我们通过获取这些填充值来判断是否符合我们的要求。同时在这个阶段，`Siri`会尝试唤醒应用来准备完成最后的处理操作。前面说了为了保证在应用和应用拓展之间能够进行通信，最好使用`frameworks`的方式来标记应用是否被启动，再进行相应操作。

  ```
  func confirm(sendMessage intent: INSendMessageIntent, completion: (INSendMessageIntentResponse) -> Void) {
        /// let content = intent.content
        /// let recipients = intent.recipients
        /// do or judge in content & recipients
        completion(INSendMessageIntentResponse(code: .success, userActivity: nil))
        /// Launch your app to do something like store message record
        /// Use a singleton in frameworks to remark if the app has launched
        /// if not launched, use the code following
        /// completion(INSendMessageIntentResponse(code: .failureRequiringAppLaunch, userActivity: nil))
    }
  ```

  `Confirm`阶段是我们最后可以尝试修改`intent`事件中传递的数值的时候。要记住一点，完全精确的内容固然是最好的答案，但是过多的让`Siri`询问用户参数的详细信息也会导致用户的抵触
- Handle阶段

  `Handle`阶段不需要做太多额外的工作，判断一下消息接收者或消息内容是否存在，如果存在，执行类似`保存/发送`的工作，然后完成。否则告诉`Siri`本次的`intent`事件处理处理失败，我们还可以通过配置` NSUserActivity  `对象来告诉`Siri`失败的原因

  ```
  func handle(sendMessage intent: INSendMessageIntent, completion: (INSendMessageIntentResponse) -> Void) {
      if intent.recipients != nil && intent.content != nil {
          /// do some thing success send message
          let success = LXDAccount.share().send(message: intent.content!, to: intent.recipients!)
          completion(INSendMessageIntentResponse(code: success, userActivity: nil))
      } else {
          let userActivity = NSUserActivity(activityType: String(INSendMessageIntent))
          userActivity.userInfo = [NSString(string: "error") : String("AppDidNotLaunch")]
          completion(INSendMessageIntentResponse(code: .failure, userActivity: userActivity))
      }
  }
  ```

## 事件UI

可以看到上面的代码主要集中在事件处理的逻辑上，那么在和`Siri`交互的过程中，我们同样可以让`Siri`展示响应的自定义界面：

![1](<https://allluckly.cn/images/blog/tuogao/tougao50/8.jpg>)

在我们创建`Intents Extension`的时候，同时`Xcode`也询问我们是否创建`Intents UI Extension`。在后者的文件目录下也有一个`Info.plist`，有着跟前面类似的键值对，差别在于后者只有一个状态的设置。

![1](<https://allluckly.cn/images/blog/tuogao/tougao50/9.jpg>)

在这个文件目录下存在一个故事板`MainInterface`，这个故事板就是`Siri`和应用交互时展示给用户看的界面。通过修改这个故事板的界面元素，就可以实现上图中的效果了。此外，在这个界面将要展示之前，我们可以修改类文件中的代码完成界面信息填充的操作：

```
func configure(with interaction: INInteraction!, context: INUIHostedViewContext, completion: ((CGSize) -> Void)!) {
    //这里执行界面设置的代码，完成之后执行completion代码就会让界面展示出来
    if let completion = completion {
        completion(self.desiredSize)
    }
}

var desiredSize: CGSize {
    return self.extensionContext!.hostedViewMaximumAllowedSize
}
```

## 尾言

在观看`WWDC2016`的新特性的时候，最开始给`Siri`和应用的交互惊艳到了。但是后来阅读文档发现这种交互仍然存在着过多的限制，整体而言并没有对`Siri`的使用带来更明显的提升。但是毫无疑问，这种交互如果苹果能继续对其进行补充发展，可以给我们的应用带来更多的新活力。

---

好文推荐：[iOS开发之超链接富文本](<https://allluckly.cn/%E6%8A%95%E7%A8%BF/tuogao42>)

