關于SiriKit
在6月14日凌晨的WWDC2016
大會上,蘋果提出iOS10
是一次里程碑并且推出了十個新特性,大部分的特性是基于iPhone
自身的原生應用的更新,具體的特性筆者不在這里再次敘述,請看客們移步WWDC2016下載自行觀賞。要說里程碑在筆者看來有些夸大其實了,不過新增的通知中心聯動3D Touch
確實為人機交互帶來新的發展,另外一個最大的亮點在于Siri
的接口開放。在iOS10
中提供了SiriKit
框架在用戶使用Siri
的時候會生成INExtension
對象來告知我們的應用,通過實現方法來讓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
事件的類。另外,官方講解中提到了
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
的使用帶來更明顯的提升。但是毫無疑問,這種交互如果蘋果能繼續對其進行補充發展,可以給我們的應用帶來更多的新活力。
文集:iOS開發