Part3: 对 Intent 的解析和处理

在这里放出原文链接地址

Part 3.1: 分析与操作 Intents

Intents Extension 的的入口是 INExtension 对象. 在你的 Extension 中, 你经常会用到3种对象来对 Intents 进行操作:

  • 一个包含了Siri 从用户那里收集了信息的Intent 对象.
  • 一个你用来分析、确认、处理 Intent 的自定义对象.
  • 一个 Response 对象. 该对象包含了你对 Intent 的响应.

SiriKit 有一个需要处理的 Intent 对象, 那么它将会要求 INExtension 来提供一个拥有处理能力的对象( Handler Objects, 在这里我们暂时将这个对象称之为处理者对象). 处理者对象遵守了指定的协议, 所有的处理Intent 的协议都有着相同的结构, 解析、确认、处理等方法.

当你在实现一个处理者对象的时候, 你必须实现处理Intent 的方法, 其他所有的方法都是可选的, 但是我推荐你去实现他们. 在你处理Intent 之前, 你可以使用分析和确认方法对你的Intent 进行内容的验证. 在这个过程中, Siri 可以帮助你和用户进行交互来收集、获取你所需要的信息.

下图中阐述了 SiriKit 和 处理者对象的交互流程. 当用户使用 Siri 对你的 App 生成了一个请求, SiriKit 将会创建一个合适的 Intent 对象. 如果这个 Intent 对象包含了需要解析的参数, SiriKit 将会告知处理者对象对参数进行逐一的解析. 当所有的参数都被解析完成后, SiriKit 将会通知你确认准备处理Intent . 如果你表示已经准备好了, 那么SiriKit 将会让你来处理这个 Intent. 此时你就可以根据 Intent 中所描述的信息内容来执行你的任务了.

图片来自官方文档

Part 3.2: 为 Intent 分配对应的处理者对象

当你的 App 接受到了一个 Intent 对象时, SiriKit 会要求 Intents Extension 提供一个合适的处理者对象. 此时 SiriKit 将会调用你自定义的 INExtension 的子类中的 handlerForIntent: 方法(该方法就是你 App Extension 的切入点). 在该方法中, 你需要返回一个可以解析、确认、处理 Intent 的处理者对象.

下面这个代码块中实现了一个 handlerForIntent: 方法, 用来提供不同场景中使用的处理者对象, 其中包含了消息场景和网络电话场景.

- (nullable id) handlerForIntent:(INIntent*)intent {
   id handler = nil;

   // You can substitute other objects for self based on the specific intent
   if ([intent isKindOfClass:[INSendMessageIntent class]] ||
       [intent isKindOfClass:[INSearchForMessagesIntent class]] ||
       [intent isKindOfClass:[INSetMessageAttributeIntent class]]) {
       handler = [[MyMessageHandler alloc] init];
   }
   else if ([intent isKindOfClass:[INStartAudioCallIntent class]]) {
       handler = [[MyAudioCallHandler alloc] init];
   }
   else if ([intent isKindOfClass:[INStartVideoCallIntent class]]) {
       handler = [[MyAudioCallHandler alloc] init];
   }

   return handler;
}

无论你需要处理什么样的 Intent , 上面这个方法的结构都是相同的. 首先你需要确认 Intent 对象的类型, 然后返回一个拥有处理该类型 Intent 的处理者对象. 你可以使用一个拥有了处理所有场景能力的处理者对象, 或者你也可以创建许多不同的处理者对象来处理不同场景的 Intent. 在上文这个例子中, MyMessageHandler 这个处理者对象就同时拥有处理 INSendMessageIntentINSearchForMessagesIntentINSetMessageAttributeIntent 场景的能力, 而 MyAudioCallHandlerMyAudioCallHandler 处理者则只能处理与其对应的某一场景.

在处理一个 Intent 的过程中, SiriKit 通常会多次向你获取处理者对象. 处理Intent 的过程通常会花费一些时间, 尤其是当 Siri 向用户获取更清晰的信息或附加信息的时候, 所以你必须确保处理者对象是一个有效的对象. 也就是说, 你返回的这个处理者对象不能改依赖于一些缓存的数据, 取而代之的, 应该是以个全新的处理者对象.

INIntentHandlerProviding 协议的文档中, 对该方法也有阐述:

When creating handler objects in this method, use the provided intent object only to determine which handler to create. Do not use the intent object to initialize your handler object and do not store a reference to the intent object for later use. SiriKit updates the intent object during the processing of the request to incorporate any new information provided by the user.

当你在该方法中创建一个处理者对象时, 仅仅使用该方法提供的 Intent 对象来确定你需要创建哪个处理者. 不要使用 Intent 对象来初始化你的处理者对象, 当然也不要为了在将来能用到这个Intent对象而对其保存一个引用. SiriKit 在这个过程中, 都会对Intent 对象进行更新, 来让 Intent 对象包含任何用户所提供的信息.

了解如何实现 handlerForIntent: 方法的更多信息, 请查看: INIntentHandlerProviding Protocol Reference

Part 3.3: 解析阶段 - 对 Intent 对象进行解析

你可以使用这个阶段来验证Intent 对象中包含的参数并确认Intent 对象中是否包含了你接下来需要的信息. 因为这些请求信息都是来自 Siri 对用户语言的解析, 所以最初的信息很有可能不够完整或者不够清晰, 那么在解析阶段, 你可以查看每一个参数, 并且告知 SiriKit 这些参数是否是你需要的, 你还可以让 Siri 向用户获取一些更进一步的信息.

大多数的 Intent 对象都包含至少一条参数来让你的处理者对象对其进行解析. 当你实现一个处理者对象的时候, 我推荐你为Intent 对象实现所有的解析方法. 尽管有可能你不会用到这个参数, 你仍然可以实现相应的方法来告知SiriKit 你没有用到这个参数. 告知SiriKit 你没有用到这个参数, 可以避免SiriKit 向用户询问有关该参数的问题.

你实现每一个解析方法时, 都应该遵循以下这种结构:

  1. 获取 Intent 对象中的参数.
  2. 验证这个参数是不是你 App 中需要用到的.
  3. 根据你的验证结果, 实例化一个合适的 INIntentResolutionResult 子类对象.
  4. 执行方法中传入的 Block, 并将你实例化的结果对象以参数的形式传入这个 Block 中.

Intent Framework 为每一种类型都定义了一个 INIntentResolutionResult的子类. 每一个子类都包含了一个类方法, 让你来指定你是解析成功了还是需要更多的信息. 父类INIntentResolutionResult中包含了方法, 你可以使用该方法来指定一些常见的处理结果. 下面表格中列出了一些你可能用得到的处理结果.

处理结果 举例说明
解析成功 当你指定这个处理结果时, 说明你可以使用提供的值对Intent 对象进行处理. 例如: 如果用户指定了一个人的名字为 John, 并且通讯录中只有一个叫John 的人, 那么你就可以指定解析成功.
不需要的值 当你指定这个处理结果时, 说明你不需要用到这个值. 使用 notRequired 类方法来初始化处理结果对象.
需要更加清晰的值 当你指定这个处理结果时, 说明你可能鉴别出了两个或两个以上可能得结果, 但是你没有办法确定哪个结果是更准确的. 这可能会导致和用户产生一个附加的交互, 让用户来选择他想要的结果.
需要用户确认的值 当你指定这个处理结果时, 说明你需要用户来最终确认你的选择, 当你鉴别出了一个值, 但是你不能完全确定这个值是否是正确值得时候, 你可以使用这个处理结果.
需要一个值 当你指定这个处理结果时, 说明你解析参数的时候并没有解析到你需要的值. 例如: 当支付过程中, 你并没有解析到支付金额的时候. 使用needsValue 类方法来初始化处理结果对象.
不支持的值 当你指定这个处理结果时, 说明你的 App 不支持这个特殊的值 或者 这个值和其他的参数发生了冲突的时候. 例如: 当支付过程中, 用户提供的是瑞士法郎, 但是你的 App 只支持欧元或美元的时候. 使用unsupported 类方法来初始化处理结果对象.

在解析参数的时候, 你需要尽可能快的得出结果. 向用户询问更多的信息会带来额外的交互, 过多的交互会给用户带来负担, 也会使用户体验非常不好. 所以你可以基于用户的习惯和喜好选择一个最为可能得值作为结果.

下面这个代码块中是一个旅行预订 App 验证飞机降落地点的 Demo. 当飞机降落的地点在你 App 提供服务的范围区域内, 则该方法会返回成功, 反之该方法将会告知 Siri 不支持这个值. 然而当值不存在的时候, 这个方法也会告知Siri, 并让Siri 向用户询问一个值.

- (void)resolveDropOffLocationForRequestRide:(INRequestRideIntent *)intent
       withCompletion:(void (^)(INPlacemarkResolutionResult *resolutionResult))completion {
   CLPlacemark* location = intent.dropOffLocation;
   INPlacemarkResolutionResult* result = nil;
 
   if (location) {
      // If the locaiton is valid, use it; otherwise,
      // let the user know it is not supported
      if ([self locationIsInsideServiceArea:location])
         result = [INPlacemarkResolutionResult successWithResolvedPlacemark:location];
      else
         result = [INPlacemarkResolutionResult unsupported];
   }
   else {
      // Ask for the drop-off location.
      result = [INPlacemarkResolutionResult needsValue];
   }
 
   // Return the result.
   completion(result);
}

Part 3.4: 确认阶段

利用该阶段对Intent 的参数进行最后的验证, 并且确认你已经准备好处理Intent 了. 在所有参数都被解析成功, SiriKit 将会让你来执行处理Intent 的操作. 你可以在确认方法( confirm method ) 中执行最后的检查. 例如: 如果你的 App 服务是基于网络请求的, 在这个方法中你可以确保网络请求服务是否正常. 确认方法(confirm method ) 不是必须实现的方法, 但是我们强烈推荐你实现它.

在你的确认方法(confirm method ) 中, 执行所有的确认操作, 然后创建一个响应对象(response object ), 该对象包含了你在处理 Intent 时需要用到的一些数据. 如果需要提示用户来确认一些信息时, Siri 将会在确认页面中展示你在相应对象( response object )中的详细信息. Siri 只有在重要的时候才会提示用户进行确认, 例如: 金钱相关的操作 和 某些不可逆的操作. 其他时候 Siri 是不会提示用户进行确认操作的.

下面代码块中, 展示了一个确认方法( confirm method ) 的简单实现. 是一个锻炼( workout ) 相关的 App. 该方法中提供了一个响应对象( response object )标识着 App 是否已经准备好了. userActivity 参数传入 nil, 将会导致 SiriKit 创建一个默认的user activity 对象.

- (void)confirmStartWorkout:(INStartWorkoutIntent *)startWorkoutIntent
                 completion:(void (^)(INStartWorkoutIntentResponse * _Nonnull))completion {
   // Make sure the app is ready.
 
   // Provide the response.
   INStartWorkoutIntentResponse* response = [[INStartWorkoutIntentResponse alloc]
            initWithCode:INStartWorkoutIntentResponseCodeReady userActivity:nil];
 
   completion(response);
}

Part 3.5: 处理阶段

处理阶段是整个流程的最后一个阶段, 该阶段将会执行 Intent 对象所关联的任务. 处理阶段有两个重要的职责:

  1. 执行 Intent 对象所关联的任务.
  2. 返回一个响应对象 ( response object ).

如何对一个Intent 对象进行处理是基于Intent 对象本身的. 尽可能的直接使用 Intents ExtensionIntent 对象进行处理. 有些Intent 是需要向你的 App 传入一个控制的(However, some intents involve passing control back to your app). 例如: 成功的处理一个开始锻炼的Intent 还涵盖了启动你的 App 并且开始锻炼的过程. 当执行完任务之后, 在 Block 中传入一个相应对象( response object )返回给SiriKit, 该对象包含了你 App 都做了哪些操作的信息.

下面代码块中展示了开始一个新的锻炼任务的实现部分. 在这种情况下, 处理者对象( handler object ) 告知 Siri 开始相关的 App 并开始锻炼任务. 其他的Intent 可能需要为相应对象( response object ) 提供一些额外的数据.

- (void)handleStartWorkout:(INStartWorkoutIntent *)startWorkoutIntent
                completion:(void (^) (INStartWorkoutIntentResponse * _Nonnull))completion {
   INStartWorkoutIntentResponse* response = [[INStartWorkoutIntentResponse alloc]
               initWithCode:INStartWorkoutIntentResponseCodeContinueInApp userActivity:nil];
 
   completion(response);
}

每一个响应者对象( response object ) 都支持一个可选的 NSUserActivity 对象, 该对象承载着如何启动你的 App 的信息. 如果你没有指定这个对象, SiriKit 将会为你创建一个默认的NSUserActivity 对象, 其中涵盖了一个 INInteraction 对象, 该对象包含了 Intent 对象和响应者对象( response object ). 如果你希望添加更多的附加信息, 你可以自己声明一个 NSUserActivity 对象, 在改对象的 userInfo 字典中添加更多的信息.

在操作 Intent 的过程中, 你做出的任何修改都应该反应在你的 App 界面中, 作为执行任务的一部分, 你的Intents Extension 应该传达一切相关的信息到它的源 App 中. 如果你的 Intents Extension 成功的处理了 Intent, 那么 user activity 对象将永远都不会传送给你的 App 了. 所以为了确保修改的信息能够被送达你的 App, 你应该更新被分享数据的业务逻辑(update any shared data structures), 使用后台 App 刷新、 静默推送或其他一些技术来将修改的信息传达给你的 App.

Part 3.6: 处理 Intent 的技巧

当你在确认方法 ( confirm method ) 或 处理方法 ( handle method ) 中返回一个响应对象 ( response object )时, 你需要考虑如下几点:

  • 尽可能多的在你的响应对象 ( response object ) 中添加信息: 为你的响应对像 ( response object ) 添加更多的详细信息来告知用户你的 App 都做了哪些操作, 这会给用户一个更好的体验.
  • 在分配给响应对象 ( response object ) 之前, 尽可能充分的配置你的数据对象: 对于大多数的响应对像 ( response object ) 而言, 它的属性都是使用 Copy 修饰的. 所以当你从属性中获取了一个数据对象并且对其进行了修改时, 实际上是修改了 copy 出来的对象, 而并不是对其本身进行了修改, 所以你应该充分的对你的数据对象进行修改后再赋值.
  • 在尽可能短的时间内返回你的相应对象 (response object ): 因为 SiriMaps 是与用户实时联系的, 所以你应该尽可能快的返回相应对象 (response object ). 如果你返回相应对象需要一段时间的话, 你应该先返回一个带有"正在进行中..."类似提示语的相应对象 (response object )来告诉用户你正在进行操作.

当可以使用通讯录的时候, SiriKit 会利用通讯录中的信息来对参数进行填充, 如果你的 App 被拒绝访问通讯录了, 那么Intent 对象中就将不会包含任何与通讯录相关的信息了. 例如: INPerson 对象将不会包含任何联系人的内容. 拒绝访问通讯录也同样会拒绝SiriKit 访问通讯录中的地址, 如此一来, 如果用户对 Siri 说: 我需要使用 <App> 导航骑车回家时, 将会导致 Intent 对象中不包含目的地. 如果 App 被用户拒绝了访问通讯录, 你可能需要提示用户: 开启通讯录权限将会为您带来更完美的用户体验.

Lemon龙说:

如果您在文章中看到了错误 或 误导大家的地方, 请您帮我指出, 我会尽快更改

如果您有什么疑问或者不懂的地方, 请留言给我, 我会尽快回复您

如果您觉得本文对您有所帮助, 您的喜欢是对我最大的鼓励

如果您有好的文章, 可以投稿给我, 让更多的 iOS Developer 在简书这个平台能够更快速的成长

推荐阅读更多精彩内容