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 中,將蘋果添加到購物清單”這樣的短語。 它還可以處理諸如“大米、洋蔥和橄欖”之類的項目列表。
幾乎完成了,只是更多的設置
所有這些都將在您的模擬器或本地設備中運行,但如果您想提交此內容,您需要在應用程序的plist
中添加一個NSSiriUsageDescription
鍵,並帶有一個描述您使用 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.