關於SiriKit
在6月14日凌晨的WWDC2016大會上,蘋果提出iOS10是一次裡程碑並且推出了十個新特性,大部分的特性是基於iPhone自身的原生應用的更新,具體的特性筆者不在這裡再次敘述,請看客們移步WWDC2016下載自行觀賞。要說裡程碑在筆者看來有些誇大其實了,不過新增的通知中心聯動3D Touch確實為人機交互帶來新的發展,另外一個最大的亮點在於Siri的接口開放。在iOS 10中提供了SiriKit框架在用戶使用Siri的時候會生成INExtension對象來告知我們的應用,通過實現方法來讓Siri獲取應用想要展示給用戶的內容。
Siri服務
在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對象確認處理結果然後執顯示結果給用戶看
具體的執行過程請參考文檔和講解視頻。
創建Intents Extension
SiriKit通過添加App Extension的方式來完成集成,這是一種獨立於應用本身運行的代碼結構,作為應用的擴展功能,只有在需要的時候系統會喚醒這些Extension代碼來執行任務,然後在執行完畢之後將其殺死。另一方面,這些Extension在運行過程中的可占用內存是較少的,並且由於調用時機的限制,我們也無法在運行期間做一些壞事
現階段集成SiriKit的條件是需要將開發工具升級到Xcode8,需要使用開發者賬號到官方網站去下載Xcode8_beta版,並且需要將一台測試設備升級到iOS10系統。選中我們的應用,進入項目總覽界面,新增一個TARGET
如上圖所示,我創建的Intents Extension被我命名為LXDSiriExtension。記住在創建好一個Extension的時候,會詢問你是否激活這個擴展,勾選是。另外還會提示你是否連同Intents UI Extension一並創建了,我們同樣選是。這樣我們在項目下面總共創建了LXDSiriExtension和LXDSiriExtensionUI兩個TARGET,這兩個文件目錄下面分別存在著一個新的info.plist文件,這個文件用來設置intent事件發生時我們設置的處理類。這裡借用WWDC在講解時的一張ppt來了解:
按圖中的層次展開,IntentsSupported和IntentsRestrictedWhileLocked分別是兩個字符串數組,每一個字符串表示的是應用擴展處理的intent事件的類名。前者表示支持的事件類型,後者表示在非鎖屏狀態下執行的事件類型。文件默認是workout類型的事件,在這裡筆者改成了發送消息INSendMessageIntent。除此之外,NSExtensionPrincipalClass對應的是INExtension子類類名,這個類用來獲取處理intent事件的類。
plist設置
另外,官方講解中提到了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展示響應的自定義界面:
在我們創建Intents Extension的時候,同時Xcode也詢問我們是否創建Intents UI Extension。在後者的文件目錄下也有一個Info.plist,有著跟前面類似的鍵值對,差別在於後者只有一個狀態的設置。
在這個文件目錄下存在一個故事板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的使用帶來更明顯的提升。但是毫無疑問,這種交互如果蘋果能繼續對其進行補充發展,可以給我們的應用帶來更多的新活力。
本文作者:伯樂在線 - 林欣達