SiriKit 的 Intent 是否适合您的应用程序? 如果是这样,这是如何使用它们
已发表: 2022-03-10自 iOS 5 以来,Siri 已经帮助 iPhone 用户使用 Apple 的应用程序发送消息、设置提醒和查找餐厅。 从 iOS 10 开始,我们也可以在我们自己的一些应用程序中使用 Siri。
为了使用此功能,您的应用程序必须符合 Apple 预定义的 Siri“域和意图”。 在本文中,我们将了解它们是什么,并查看我们的应用程序是否可以使用它们。 我们将使用一个简单的应用程序,它是一个待办事项列表管理器,并学习如何添加 Siri 支持。 我们还将浏览 Apple 开发者网站关于配置和 Swift 代码的指南,以了解 SiriKit 引入的一种新型扩展: Intents扩展。
当您进入本文的编码部分时,您将需要 Xcode(至少 9.x 版本),如果您熟悉 Swift 中的 iOS 开发会很好,因为我们将在一个小型工作中添加 Siri应用程序。 我们将完成在 Apple 开发者网站上设置扩展程序并将 Siri 扩展程序代码添加到应用程序的步骤。
“嘿 Siri,我为什么需要你?”
有时我在沙发上使用手机,双手解放,我可以全神贯注于屏幕。 也许我会给我姐姐发短信来计划我们妈妈的生日,或者在 Trello 中回答问题。 我可以看到应用程序。 我可以点击屏幕。 我可以打字。
但是当我的手表上出现一条短信时,我可能正在我的城镇里走来走去,听播客。 我的手机在口袋里,走路时无法轻松接听。
使用 Siri,我可以按住耳机的控制按钮说:“给姐姐发短信说我会在两点前到。” 当您在旅途中无法全神贯注于您的手机或互动较少时,Siri 非常棒,但它需要多次轻敲和大量打字。
如果我想使用 Apple 应用程序进行这些交互,这很好。 但是某些类别的应用程序(例如消息传递)具有非常受欢迎的替代方案。 其他活动,例如预订乘车或在餐厅预订餐桌,甚至无法使用 Apple 的内置应用程序进行,但对于 Siri 来说是完美的。
苹果的语音助手方法
为了在第三方应用程序中启用 Siri,Apple 必须决定一种机制,从用户的声音中获取声音,并以某种方式将其传递给应用程序,以满足请求。 为了实现这一点,Apple 要求用户在请求中提及应用程序的名称,但对于如何处理请求的其余部分,他们有多种选择。
- 它可能已经向应用程序发送了一个声音文件。
这种方法的好处是应用程序可以尝试处理用户可能对其提出的任何请求。 亚马逊或谷歌可能喜欢这种方法,因为他们已经拥有复杂的语音识别服务。 但大多数应用程序无法轻松处理此问题。 - 它本可以将语音变成文本并发送出去。
由于许多应用程序没有复杂的自然语言实现,用户通常必须坚持使用非常特殊的短语,而非英语支持将由应用程序开发人员来实现。 - 它可能会要求您提供您理解的短语列表。
这种机制更接近于亚马逊对 Alexa 所做的(在其“技能”框架中),并且它使 Alexa 的使用比 SiriKit 目前所能处理的要多得多。 在 Alexa 技能中,您提供带有 Alexa 将为您填写的占位符变量的短语。 例如,“Alexa,在$TIME$
到$REMINDER$
时提醒我”——Alexa 将根据用户所说的内容运行此短语,并告诉您TIME
和REMINDER
的值。 与之前的机制一样,开发人员需要完成所有的翻译,如果用户说的内容略有不同,则没有太大的灵活性。 - 它可以定义带有参数的请求列表并向应用程序发送结构化请求。
这实际上是 Apple 所做的,其好处是它可以支持多种语言,并且它所做的所有工作都是为了尝试理解用户可能会表达请求的所有方式。 最大的缺点是您只能为 Apple 定义的请求实现处理程序。 例如,如果你有一个消息应用程序,这很好,但如果你有音乐流媒体服务或播客播放器,你现在无法使用 SiriKit。
同样,应用程序可以通过三种方式与用户对话:通过声音、通过转换后的文本,或者通过表达你想说的话并让系统找出表达它的确切方式。 最后一个解决方案(这是 Apple 所做的)将翻译的负担放在了 Apple 身上,但它为您提供了有限的方式来使用自己的话来描述事物。
您可以处理的请求类型在 SiriKit 的域和意图中定义。 意图是用户可能提出的一种请求,例如给联系人发短信或查找照片。 每个意图都有一个参数列表——例如,发短信需要联系人和消息。
域只是一组相关的意图。 阅读文本和发送文本都在消息传递域中。 预订行程和获取位置属于行程预订域。 有用于拨打 VoIP 电话、开始锻炼、搜索照片和其他一些事情的域。 SiriKit 的文档包含域及其意图的完整列表。
对 Siri 的一个普遍批评是,它似乎无法像谷歌和 Alexa 一样处理请求,而且苹果竞争对手启用的第三方语音生态系统更丰富。
我同意这些批评。 如果您的应用程序不符合当前意图,那么您将无法使用 SiriKit,您也无能为力。 即使您的应用程序确实适合,您也无法控制 Siri 所说或理解的所有单词; 所以,如果你在你的应用中有一种特定的谈论方式,你就不能总是把它教给 Siri。
iOS 开发人员的希望是,Apple 将大大扩展其意图列表,并希望其自然语言处理变得更好。 如果确实如此,那么我们将拥有一个语音助手,无需开发人员进行翻译或理解所有表达相同事物的方式即可工作。 实现对结构化请求的支持实际上相当简单——比构建自然语言解析器要容易得多。
意图框架的另一大好处是它不仅限于 Siri 和语音请求。 即使是现在,地图应用程序也可以为您的应用程序生成基于意图的请求(例如,餐厅预订)。 它以编程方式执行此操作(不是来自语音或自然语言)。 如果 Apple 允许应用程序发现彼此暴露的意图,我们将有更好的方式让应用程序协同工作(与 x-callback 样式的 URL 不同)。
最后,由于意图是带有参数的结构化请求,因此应用程序可以通过一种简单的方式来表达缺少参数或需要帮助来区分某些选项。 然后,Siri 可以提出后续问题来解决参数,而无需应用程序进行对话。
乘车预订域
要了解域和意图,让我们看一下乘车预订域。 这是您用来请求 Siri 为您提供 Lyft 汽车的域。
Apple 定义了如何叫车以及如何获取有关它的信息,但实际上没有内置的 Apple 应用程序可以实际处理此请求。 这是少数需要启用 SiriKit 的应用程序的领域之一。
您可以通过语音或直接从地图调用其中一个意图。 该域的一些意图是:
- 叫车
使用这个预订一程。 您需要提供接送地点,并且该应用程序可能还需要知道您的聚会人数以及您想要什么样的乘车方式。 一个示例短语可能是“通过 <appname> 为我预订一程”。 - 获取行程状态
使用此意图查明您的请求是否已收到,并获取有关车辆和驾驶员的信息,包括他们的位置。 地图应用程序使用此意图显示汽车接近您时的更新图像。 - 取消行程
使用它来取消您已预订的行程。
对于任何此类意图,Siri 可能需要了解更多信息。 当我们实现一个意图处理程序时,您将看到,您的 Intents 扩展可以告诉 Siri 缺少必需的参数,Siri 会提示用户输入它。
可以通过 Maps 以编程方式调用 Intent 的事实表明 Intent 如何在未来启用应用间通信。
注意:您可以在 Apple 的开发者网站上获得完整的域列表及其意图。 还有一个示例 Apple 应用程序,其中实现了许多域和意图,包括乘车预订。
向您的应用程序添加列表和备注域支持
好的,现在我们了解了 SiriKit 的基础知识,让我们看看您将如何在涉及大量配置的应用程序中添加对 Siri 的支持,并为您要处理的每个意图提供一个类。
本文的其余部分包含将 Siri 支持添加到应用程序的详细步骤。 您需要做五件高级别的事情:
- 通过在 Apple 的开发者网站上为应用程序创建具有新权利的配置文件,准备向应用程序添加新扩展。
- 配置您的应用程序(通过其
plist
)以使用权利。 - 使用 Xcode 的模板开始使用一些示例代码。
- 添加代码以支持您的 Siri 意图。
- 通过
plist
配置 Siri 的词汇表。
不用担心:我们将逐一介绍这些内容,并在此过程中解释扩展和权利。
为了只关注 Siri 部分,我准备了一个简单的待办事项列表管理器 List-o-Mat。
您可以在 GitHub 上找到示例 List-o-Mat 的完整源代码。
为了创建它,我所做的只是从 Xcode Master-Detail 应用程序模板开始,并将两个屏幕都制作成UITableView
。 我添加了一种添加和删除列表和项目的方法,以及一种将项目核对为已完成的方法。 所有的导航都是由模板生成的。
为了存储数据,我使用了Codable
协议(在 WWDC 2017 中引入),它将结构转换为 JSON 并将其保存在documents
文件夹中的文本文件中。
我刻意保持代码非常简单。 如果你有任何使用 Swift 和制作视图控制器的经验,那么你应该没有问题。
现在我们可以完成添加 SiriKit 支持的步骤。 对于任何应用程序以及您计划实施的任何域和意图,高级步骤都是相同的。 我们将主要与 Apple 的开发者网站打交道,编辑plist
并编写一些 Swift。
对于 List-o-Mat,我们将专注于列表和笔记领域,它广泛适用于笔记应用程序和待办事项列表等内容。
在列表和注释域中,我们有以下对我们的应用有意义的意图。
- 获取任务列表。
- 将新任务添加到列表中。
因为与 Siri 的交互实际上发生在您的应用程序之外(甚至可能在您的应用程序未运行时),iOS 使用扩展来实现这一点。
意图扩展
如果你没有使用过扩展,你需要知道三个主要的事情:
- 扩展是一个单独的过程。 它在您的应用程序包中交付,但它完全独立运行,具有自己的沙箱。
- 您的应用程序和扩展程序可以通过在同一个应用程序组中相互通信。 最简单的方法是通过该组的共享沙箱文件夹(因此,如果您将它们放在那里,它们可以读取和写入相同的文件)。
- 扩展程序需要它们自己的应用程序 ID、配置文件和权利。
要为您的应用添加扩展程序,首先登录您的开发者帐户并转到“证书、标识符和配置文件”部分。
更新您的 Apple Developer App 帐户数据
在我们的 Apple 开发者帐户中,我们需要做的第一件事就是创建一个应用组。 转到“标识符”下的“应用程序组”部分并添加一个。
它必须以group
开头,后跟通常的基于域的反向标识符。 因为它有一个前缀,所以您可以使用您的应用程序的标识符作为其余部分。
然后,我们需要更新应用的 ID 以使用该组并启用 Siri:
- 转到“App IDs”部分,然后单击您的应用程序 ID;
- 点击“编辑”按钮;
- 启用应用程序组(如果未为其他扩展启用)。
- 然后通过单击“编辑”按钮配置应用程序组。 选择之前的应用组。
- 启用 SiriKit。
- 单击“完成”以保存它。
现在,我们需要为我们的扩展创建一个新的应用 ID:
- 在同一个“App IDs”部分,添加一个新的应用 ID。 这将是您的应用的标识符,带有后缀。 不要只使用
Intents
作为后缀,因为这个名称将成为您的模块在 Swift 中的名称,然后会与真正的Intents
冲突。 - 也为应用组启用此应用 ID(并像以前一样设置组)。
现在,为 Intents 扩展创建开发配置文件,并重新生成应用程序的配置文件。 像往常一样下载并安装它们。
现在我们的配置文件已安装,我们需要转到 Xcode 并更新应用程序的权利。
在 Xcode 中更新应用程序的权利
返回 Xcode,在项目导航器中选择您的项目名称。 然后,选择您的应用程序的主要目标,然后转到“功能”选项卡。 在那里,您会看到一个打开 Siri 支持的开关。
在列表的下方,您可以打开应用程序组并对其进行配置。
如果您已正确设置,您将在应用的.entitlements
文件中看到:
现在,我们终于准备好将 Intents 扩展目标添加到我们的项目中了。
添加意图扩展
我们终于准备好添加扩展了。 在 Xcode 中,选择“文件”→“新目标”。 将弹出此表:
选择“意图扩展”,然后单击“下一步”按钮。 填写以下屏幕:
产品名称需要与您在 Apple 开发者网站上的意图应用 ID 中添加的任何后缀匹配。
我们选择不添加意图 UI 扩展。 本文未对此进行介绍,但如果需要,您可以稍后添加。 基本上,这是一种将您自己的品牌和显示风格融入 Siri 视觉结果的方式。
完成后,Xcode 将创建一个意图处理程序类,我们可以将其用作 Siri 实现的起始部分。
Intents Handler:解决、确认和处理
Xcode 生成了一个新目标,为我们提供了一个起点。
您要做的第一件事是将这个新目标设置为与应用程序在同一个应用程序组中。 和以前一样,转到目标的“功能”选项卡,打开应用程序组,并使用您的组名进行配置。 请记住,同一组中的应用程序有一个沙箱,它们可以用来相互共享文件。 我们需要这个才能让 Siri 请求到达我们的应用程序。
List-o-Mat 具有返回组文档文件夹的功能。 每当我们想要读取或写入共享文件时,我们都应该使用它。
func documentsFolder() -> URL? { return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.app-o-mat.ListOMat") }
例如,当我们保存列表时,我们使用这个:
func save(lists: Lists) { guard let docsDir = documentsFolder() else { fatalError("no docs dir") } let url = docsDir.appendingPathComponent(fileName, isDirectory: false) // Encode lists as JSON and save to url }
Intents 扩展模板创建了一个名为IntentHandler.swift
的文件,其中包含一个名为IntentHandler
的类。 它还将其配置为扩展plist
中的意图入口点。
在同一个plist
中,您将看到一个部分来声明我们支持的意图。 我们将从允许搜索列表的那个开始,它被命名为INSearchForNotebookItemsIntent
。 将其添加到IntentsSupported
下的数组中。
现在,转到IntentHandler.swift
并将其内容替换为以下代码:
import Intents class IntentHandler: INExtension { override func handler(for intent: INIntent) -> Any? { switch intent { case is INSearchForNotebookItemsIntent: return SearchItemsIntentHandler() default: return nil } } }
调用handler
函数以获取对象以处理特定意图。 您可以在此类中实现所有协议并返回self
,但我们会将每个意图放在其自己的类中以使其更好地组织。
因为我们打算有几个不同的类,所以让我们给它们一个通用的基类,用于我们需要在它们之间共享的代码:
class ListOMatIntentsHandler: NSObject { }
意图框架要求我们从NSObject
继承。 我们稍后会填写一些方法。
我们从这个开始我们的搜索实现:
class SearchItemsIntentHandler: ListOMatIntentsHandler, INSearchForNotebookItemsIntentHandling { }
要设置一个意图处理程序,我们需要实现三个基本步骤
- 解决参数。
确保提供了所需的参数,并消除您不完全理解的任何参数。 - 确认请求是可行的。
这通常是可选的,但即使您知道每个参数都很好,您可能仍需要访问外部资源或有其他要求。 - 处理请求。
做被要求的事情。
INSearchForNotebookItemsIntent
是我们将实现的第一个意图,可用作任务搜索。 我们可以处理的请求类型是“在 List-o-Mat 中,显示杂货店列表”或“在 List-o-Mat 中,显示商店列表”。
顺便说一句:“List-o-Mat”实际上是 SiriKit 应用程序的一个坏名字,因为 Siri 很难在应用程序中使用连字符。 幸运的是,SiriKit 允许我们使用备用名称并提供发音。 在应用程序的Info.plist
,添加以下部分:
这允许用户说“list oh mat”并将其理解为一个单词(不带连字符)。 它在屏幕上看起来并不理想,但如果没有它,Siri 有时会认为“List”和“Mat”是两个单独的词,并且会感到非常困惑。
解决:弄清楚参数
对于笔记本项目的搜索,有几个参数:
- 项目类型(任务、任务列表或注释),
- 项目的标题,
- 项目的内容,
- 完成状态(任务是否标记为完成),
- 与之关联的位置,
- 它关联的日期。
我们只需要前两个,所以我们需要为它们编写解析函数。 INSearchForNotebookItemsIntent
有方法供我们实现。
因为我们只关心显示任务列表,所以我们会将其硬编码到项目类型的解析中。 在SearchItemsIntentHandler
中,添加:
func resolveItemType(for intent: INSearchForNotebookItemsIntent, with completion: @escaping (INNotebookItemTypeResolutionResult) -> Void) { completion(.success(with: .taskList)) }
因此,无论用户说什么,我们都会搜索任务列表。 如果我们想扩展我们的搜索支持,我们会让 Siri 尝试从原始短语中找出这一点,然后在缺少项目类型时使用completion(.needsValue())
。 或者,我们可以通过查看与它匹配的内容来尝试从标题中猜测。 在这种情况下,当 Siri 知道它是什么时,我们会成功完成,当我们要尝试多种可能性时,我们会使用completion(.notRequired())
。
标题解析有点棘手。 我们想要的是 Siri 使用一个列表,如果它找到一个与你所说的完全匹配的列表。 如果不确定或存在不止一种可能性,那么我们希望 Siri 向我们寻求帮助以解决这个问题。 为此,SiriKit 提供了一组分辨率枚举,让我们表达我们接下来想要发生的事情。
所以,如果你说“杂货店”,那么 Siri 就会完全匹配。 但如果你说“商店”,那么 Siri 会显示一个匹配列表的菜单。
我们将从这个函数开始给出基本结构:
func resolveTitle(for intent: INSearchForNotebookItemsIntent, with completion: @escaping (INSpeakableStringResolutionResult) -> Void) { guard let title = intent.title else { completion(.needsValue()) return } let possibleLists = getPossibleLists(for: title) completeResolveListName(with: possibleLists, for: title, with: completion) }
我们将在ListOMatIntentsHandler
基类中实现getPossibleLists(for:)
和completeResolveListName(with:for:with:)
。
getPossibleLists(for:)
需要尝试将 Siri 传递给我们的标题与实际列表名称进行模糊匹配。
public func getPossibleLists(for listName: INSpeakableString) -> [INSpeakableString] { var possibleLists = [INSpeakableString]() for l in loadLists() { if l.name.lowercased() == listName.spokenPhrase.lowercased() { return [INSpeakableString(spokenPhrase: l.name)] } if l.name.lowercased().contains(listName.spokenPhrase.lowercased()) || listName.spokenPhrase.lowercased() == "all" { possibleLists.append(INSpeakableString(spokenPhrase: l.name)) } } return possibleLists }
我们遍历所有列表。 如果我们得到完全匹配,我们将返回它,如果没有,我们将返回一个可能性数组。 在这个函数中,我们只是检查用户所说的单词是否包含在列表名称中(因此,非常简单的匹配)。 这让“杂货店”匹配“杂货店”。 更高级的算法可能会尝试根据发音相同的单词进行匹配(例如,使用 Soundex 算法),
completeResolveListName(with:for:with:)
负责决定如何处理这个可能性列表。
public func completeResolveListName(with possibleLists: [INSpeakableString], for listName: INSpeakableString, with completion: @escaping (INSpeakableStringResolutionResult) -> Void) { switch possibleLists.count { case 0: completion(.unsupported()) case 1: if possibleLists[0].spokenPhrase.lowercased() == listName.spokenPhrase.lowercased() { completion(.success(with: possibleLists[0])) } else { completion(.confirmationRequired(with: possibleLists[0])) } default: completion(.disambiguation(with: possibleLists)) } }
如果我们得到完全匹配,我们会告诉 Siri 我们成功了。 如果我们得到一个不精确的匹配,我们会告诉 Siri 询问用户我们是否猜对了。
如果我们有多个匹配项,那么我们使用completion(.disambiguation(with: possibleLists))
告诉 Siri 显示一个列表并让用户选择一个。
现在我们知道了请求是什么,我们需要查看整个事情并确保我们可以处理它。
确认:检查所有依赖项
在这种情况下,如果我们已经解析了所有参数,我们总是可以处理请求。 典型的confirm()
实现可能会检查外部服务的可用性或检查授权级别。
因为confirm()
是可选的,所以我们什么都不做,Siri 会假设我们可以处理任何带有解析参数的请求。 明确地说,我们可以这样使用:
func confirm(intent: INSearchForNotebookItemsIntent, completion: @escaping (INSearchForNotebookItemsIntentResponse) -> Void) { completion(INSearchForNotebookItemsIntentResponse(code: .success, userActivity: nil)) }
这意味着我们可以处理任何事情。
处理:做它
最后一步是处理请求。
func handle(intent: INSearchForNotebookItemsIntent, completion: @escaping (INSearchForNotebookItemsIntentResponse) -> Void) { guard let title = intent.title, let list = loadLists().filter({ $0.name.lowercased() == title.spokenPhrase.lowercased()}).first else { completion(INSearchForNotebookItemsIntentResponse(code: .failure, userActivity: nil)) return } let response = INSearchForNotebookItemsIntentResponse(code: .success, userActivity: nil) response.tasks = list.items.map { return INTask(title: INSpeakableString(spokenPhrase: $0.name), status: $0.done ? INTaskStatus.completed : INTaskStatus.notCompleted, taskType: INTaskType.notCompletable, spatialEventTrigger: nil, temporalEventTrigger: nil, createdDateComponents: nil, modifiedDateComponents: nil, identifier: "\(list.name)\t\($0.name)") } completion(response) }
首先,我们根据标题找到列表。 此时, resolveTitle
已经确保我们将获得完全匹配。 但是如果有问题,我们仍然可以返回失败。
当我们遇到失败时,我们可以选择传递用户活动。 如果您的应用程序使用 Handoff 并且有办法处理这种确切类型的请求,那么 Siri 可能会尝试推迟到您的应用程序以在那里尝试请求。 当我们在纯语音环境中时(例如,您从“Hey Siri”开始),它不会这样做,并且它不能保证在其他情况下它会这样做,所以不要指望它。
现在可以进行测试了。 在 Xcode 的目标列表中选择意图扩展。 但在运行它之前,请编辑方案。
这带来了一种直接提供查询的方法:
请注意,由于上面提到的连字符问题,我正在使用“ListOMat”。 幸运的是,它的发音与我的应用程序名称相同,因此应该不是什么大问题。
回到应用程序,我制作了一个“杂货店”列表和一个“硬件商店”列表。 如果我向 Siri 询问“商店”列表,它将通过消歧路径,如下所示:
如果您说“Grocery Store”,那么您将得到一个完全匹配的结果,这会直接出现在结果中。
通过 Siri 添加项目
现在我们了解了解析、确认和处理的基本概念,我们可以快速添加意图以将项目添加到列表中。
首先,将INAddTasksIntent
添加到扩展的 plist 中:
然后,更新我们的IntentHandler
的handle
函数。
override func handler(for intent: INIntent) -> Any? { switch intent { case is INSearchForNotebookItemsIntent: return SearchItemsIntentHandler() case is INAddTasksIntent: return AddItemsIntentHandler() default: return nil } }
为新类添加一个存根:
class AddItemsIntentHandler: ListOMatIntentsHandler, INAddTasksIntentHandling { }
添加项目需要类似的搜索resolve
,除了目标任务列表而不是标题。
func resolveTargetTaskList(for intent: INAddTasksIntent, with completion: @escaping (INTaskListResolutionResult) -> Void) { guard let title = intent.targetTaskList?.title else { completion(.needsValue()) return } let possibleLists = getPossibleLists(for: title) completeResolveTaskList(with: possibleLists, for: title, with: completion) }
completeResolveTaskList
就像completeResolveListName
一样,但类型略有不同(任务列表而不是任务列表的标题)。
public func completeResolveTaskList(with possibleLists: [INSpeakableString], for listName: INSpeakableString, with completion: @escaping (INTaskListResolutionResult) -> Void) { let taskLists = possibleLists.map { return INTaskList(title: $0, tasks: [], groupName: nil, createdDateComponents: nil, modifiedDateComponents: nil, identifier: nil) } switch possibleLists.count { case 0: completion(.unsupported()) case 1: if possibleLists[0].spokenPhrase.lowercased() == listName.spokenPhrase.lowercased() { completion(.success(with: taskLists[0])) } else { completion(.confirmationRequired(with: taskLists[0])) } default: completion(.disambiguation(with: taskLists)) } }
它具有相同的消歧逻辑并且行为方式完全相同。 说“商店”需要消除歧义,说“杂货店”将是完全匹配的。
我们将保留未实现的confirm
并接受默认值。 对于handle
,我们需要在列表中添加一个项目并保存它。
func handle(intent: INAddTasksIntent, completion: @escaping (INAddTasksIntentResponse) -> Void) { var lists = loadLists() guard let taskList = intent.targetTaskList, let listIndex = lists.index(where: { $0.name.lowercased() == taskList.title.spokenPhrase.lowercased() }), let itemNames = intent.taskTitles, itemNames.count > 0 else { completion(INAddTasksIntentResponse(code: .failure, userActivity: nil)) return } // Get the list var list = lists[listIndex] // Add the items var addedTasks = [INTask]() for item in itemNames { list.addItem(name: item.spokenPhrase, at: list.items.count) addedTasks.append(INTask(title: item, status: .notCompleted, taskType: .notCompletable, spatialEventTrigger: nil, temporalEventTrigger: nil, createdDateComponents: nil, modifiedDateComponents: nil, identifier: nil)) } // Save the new list lists[listIndex] = list save(lists: lists) // Respond with the added items let response = INAddTasksIntentResponse(code: .success, userActivity: nil) response.addedTasks = addedTasks completion(response) }
我们得到一个项目列表和一个目标列表。 我们查找列表并添加项目。 我们还需要为 Siri 准备一个响应,以显示添加的项目并将其发送到完成功能。
该函数可以处理类似“在 ListOMat 中,将苹果添加到购物清单”这样的短语。 它还可以处理诸如“大米、洋葱和橄榄”之类的项目列表。
几乎完成了,只是更多的设置
所有这些都将在您的模拟器或本地设备中运行,但如果您想提交此内容,您需要将NSSiriUsageDescription
键添加到您的应用程序的plist
中,并带有一个描述您使用 Siri 的字符串。 诸如“您对列表的请求将发送给 Siri”之类的内容很好。
您还应该添加对以下内容的调用:
INPreferences.requestSiriAuthorization { (status) in }
把它放在你的主视图控制器的viewDidLoad
中,要求用户访问 Siri。 This will show the message you configured above and also let the user know that they could be using Siri for this app.
Finally, you'll need to tell Siri what to tell the user if the user asks what your app can do, by providing some sample phrases:
- Create a
plist
file in your app (not the extension), namedAppIntentVocabulary.plist
. - Fill out the intents and phrases that you support.
There is no way to really know all of the phrases that Siri will use for an intent, but Apple does provide a few samples for each intent in its documentation. The sample phrases for task-list searching show us that Siri can understand “Show me all my notes on <appName>,” but I found other phrases by trial and error (for example, Siri understands what “lists” are too, not just notes).
概括
As you can see, adding Siri support to an app has a lot of steps, with a lot of configuration. But the code needed to handle the requests was fairly simple.
There are a lot of steps, but each one is small, and you might be familiar with a few of them if you have used extensions before.
Here is what you'll need to prepare for a new extension on Apple's developer website:
- Make an app ID for an Intents extension.
- Make an app group if you don't already have one.
- Use the app group in the app ID for the app and extension.
- Add Siri support to the app's ID.
- Regenerate the profiles and download them.
And here are the steps in Xcode for creating Siri's Intents extension:
- Add an Intents extension using the Xcode template.
- Update the entitlements of the app and extension to match the profiles (groups and Siri support).
- Add your intents to the extension's
plist
.
And you'll need to add code to do the following things:
- Use the app group sandbox to communicate between the app and extension.
- Add classes to support each intent with resolve, confirm and handle functions.
- Update the generated
IntentHandler
to use those classes. - Ask for Siri access somewhere in your app.
Finally, there are some Siri-specific configuration settings:
- Add the Siri support security string to your app's
plist
. - Add sample phrases to an
AppIntentVocabulary.plist
file in your app. - Run the intent target to test; edit the scheme to provide the phrase.
OK, that is a lot, but if your app fits one of Siri's domains, then users will expect that they can interact with it via voice. And because the competition for voice assistants is so good, we can only expect that WWDC 2018 will bring a bunch more domains and, hopefully, much better Siri.
延伸阅读
- “SiriKit,” Apple
The technical documentation contains the full list of domains and intents. - “Guides and Sample Code,” Apple
Includes code for many domains. - “Introducing SiriKit” (video, Safari only), WWDC 2016 Apple
- “What's New in SiriKit” (video, Safari only), WWDC 2017, Apple
Apple introduces lists and notes - “Lists and Notes,” Apple
The full list of lists and notes intents.