關(guān)于SiriKit
在6月14日凌晨的WWDC2016大會(huì)上,蘋果提出iOS10是一次里程碑并且推出了十個(gè)新特性,大部分的特性是基于iPhone自身的原生應(yīng)用的更新,具體的特性筆者不在這里再次敘述,請(qǐng)看客們移步WWDC2016下載自行觀賞。要說(shuō)里程碑在筆者看來(lái)有些夸大其實(shí)了,不過(guò)新增的通知中心聯(lián)動(dòng)3D Touch確實(shí)為人機(jī)交互帶來(lái)新的發(fā)展,另外一個(gè)最大的亮點(diǎn)在于Siri的接口開放。在iOS10中提供了SiriKit框架在用戶使用Siri的時(shí)候會(huì)生成INExtension對(duì)象來(lái)告知我們的應(yīng)用,通過(guò)實(shí)現(xiàn)方法來(lái)讓Siri獲取應(yīng)用想要展示給用戶的內(nèi)容

在iOS10之后,蘋果希望Siri能夠給用戶帶來(lái)更多的功能體驗(yàn),基于這個(gè)出發(fā)點(diǎn),新增了SiriKit框架。Siri通過(guò)語(yǔ)言處理系統(tǒng)對(duì)用戶發(fā)出的對(duì)話請(qǐng)求進(jìn)行解析之后生成一個(gè)用來(lái)描述對(duì)話內(nèi)容的Intents事件,然后通過(guò)SiriKit框架分發(fā)給集成框架的應(yīng)用程序以此來(lái)獲取應(yīng)用的內(nèi)容,比如完成類似通過(guò)文字匹配查找應(yīng)用聊天記錄、聊天對(duì)象的功能,此外它還支持為用戶使用蘋果地圖時(shí)提供應(yīng)用內(nèi)置服務(wù)等功能。通過(guò)官方文檔我們可以看到SiriKit框架支持的六類服務(wù)分別是:
- 語(yǔ)音和視頻通話
- 發(fā)送消息
- 收款或者付款
- 圖片搜索
- 管理鍛煉
- 行程預(yù)約
Siri和Maps通過(guò)Intents extension的擴(kuò)展方式和我們的應(yīng)用進(jìn)行交互,其中,類型為INExtension的對(duì)象扮演著Intents extension擴(kuò)展中直接協(xié)同Siri對(duì)象共同響應(yīng)用戶請(qǐng)求的關(guān)鍵角色。當(dāng)我們實(shí)現(xiàn)了Intents extension擴(kuò)展并產(chǎn)生了一個(gè)Siri請(qǐng)求事件時(shí),一個(gè)典型的Intent事件的處理過(guò)程中總共有這三個(gè)步驟Resolve、Confirm和Handle:
Resolve階段。在Siri獲取到用戶的語(yǔ)音輸入之后,生成一個(gè)INIntent對(duì)象,將語(yǔ)音中的關(guān)鍵信息提取出來(lái)并且填充對(duì)應(yīng)的屬性。這個(gè)對(duì)象在稍后會(huì)傳遞給我們?cè)O(shè)置好的INExtension子類對(duì)象進(jìn)行處理,根據(jù)子類遵循的不同服務(wù)protocol來(lái)選擇不同的解決方案Confirm階段。在上一個(gè)階段通過(guò)handler(for intent:)返回了處理intent的對(duì)象,此階段會(huì)依次調(diào)用confirm打頭的實(shí)例方法來(lái)判斷Siri填充的信息是否完成。匹配的判斷結(jié)果包括Exactly one match、Two or more matches以及No match三種情況。這個(gè)過(guò)程中可以讓Siri向用戶征求更具體的參數(shù)信息在
confirm方法執(zhí)行完成之后,Siri進(jìn)行最后的處理階段,生成答復(fù)對(duì)象,并且向此intent對(duì)象確認(rèn)處理結(jié)果然后執(zhí)顯示結(jié)果給用戶看

具體的執(zhí)行過(guò)程請(qǐng)參考文檔和講解視頻
創(chuàng)建Intents Extension
SiriKit通過(guò)添加App Extension的方式來(lái)完成集成,這是一種獨(dú)立于應(yīng)用本身運(yùn)行的代碼結(jié)構(gòu),作為應(yīng)用的擴(kuò)展功能,只有在需要的時(shí)候系統(tǒng)會(huì)喚醒這些Extension代碼來(lái)執(zhí)行任務(wù),然后在執(zhí)行完畢之后將其殺死。另一方面,這些Extension在運(yùn)行過(guò)程中的可占用內(nèi)存是較少的,并且由于調(diào)用時(shí)機(jī)的限制,我們也無(wú)法在運(yùn)行期間做一些壞事

現(xiàn)階段集成
SiriKit的條件是需要將開發(fā)工具升級(jí)到Xcode8,需要使用開發(fā)者賬號(hào)到官方網(wǎng)站去下載Xcode8_beta版,并且需要將一臺(tái)測(cè)試設(shè)備升級(jí)到iOS10系統(tǒng)。選中我們的應(yīng)用,進(jìn)入項(xiàng)目總覽界面,新增一個(gè)TARGET

如上圖所示,我創(chuàng)建的
Intents Extension被我命名為LXDSiriExtension。記住在創(chuàng)建好一個(gè)Extension的時(shí)候,會(huì)詢問(wèn)你是否激活這個(gè)擴(kuò)展,勾選是。另外還會(huì)提示你是否連同Intents UI Extension一并創(chuàng)建了,我們同樣選是。這樣我們?cè)陧?xiàng)目下面總共創(chuàng)建了LXDSiriExtension和LXDSiriExtensionUI兩個(gè)TARGET,這兩個(gè)文件目錄下面分別存在著一個(gè)新的info.plist文件,這個(gè)文件用來(lái)設(shè)置intent事件發(fā)生時(shí)我們?cè)O(shè)置的處理類。這里借用WWDC在講解時(shí)的一張ppt來(lái)了解:
按圖中的層次展開,
IntentsSupported和IntentsRestrictedWhileLocked分別是兩個(gè)字符串?dāng)?shù)組,每一個(gè)字符串表示的是應(yīng)用擴(kuò)展處理的intent事件的類名。前者表示支持的事件類型,后者表示在非鎖屏狀態(tài)下執(zhí)行的事件類型。文件默認(rèn)是workout類型的事件,在這里筆者改成了發(fā)送消息INSendMessageIntent。除此之外,NSExtensionPrincipalClass對(duì)應(yīng)的是INExtension子類類名,這個(gè)類用來(lái)獲取處理intent事件的類。
另外,官方講解中提到了
Embedded frameworks,在session中蘋果開發(fā)人員通過(guò)一個(gè)消息聊天應(yīng)用來(lái)示例集成SiriKit。由于應(yīng)用擴(kuò)展自身的運(yùn)行機(jī)制和應(yīng)用本身的運(yùn)行機(jī)制不同,彼此之間創(chuàng)建的類是不能訪問(wèn)使用的。因此把我們需要的類開發(fā)成frameworks的方式導(dǎo)入我們的應(yīng)用之后就能夠在兩種之中都使用到這些類。本文未使用frameworks導(dǎo)入功能,而是模擬了一個(gè)類用來(lái)管理事件處理過(guò)程中的部分邏輯,但是Embedded frameworks這個(gè)使用的準(zhǔn)則需要記住。這個(gè)模擬類的具體代碼如下:
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
}
}
在完成這些需要的工作之后,我們還需要對(duì)應(yīng)用本身的Info.plist配置文件進(jìn)行設(shè)置,新增一個(gè)關(guān)鍵字為NSSiriUsageDescription的字符串對(duì)象,對(duì)應(yīng)填寫的字符串將在我們征詢用戶Siri權(quán)限的時(shí)候顯示給用戶看。比如Siri想要訪問(wèn)您的應(yīng)用信息之類的提示語(yǔ)。然后通過(guò)INPreferences類方法向用戶請(qǐng)求Siri訪問(wèn)權(quán)限
import Intents
INPreferences.requestSiriAuthorization {
switch $0 {
case .authorized:
print("用戶已授權(quán)")
break
case .notDetermined:
print("未決定")
break
case .restricted:
print("權(quán)限受限制")
break
case .denied:
print("拒絕授權(quán)")
break
}
}
代碼實(shí)現(xiàn)
首先我們需要一個(gè)INExtension的子類,你也可以在默認(rèn)創(chuàng)建的子類中實(shí)現(xiàn)代碼。在方法中,我們通過(guò)判斷intent的類型來(lái)創(chuàng)建對(duì)應(yīng)的處理者實(shí)例,然后返回。在本文的示例中,假設(shè)我們對(duì)Siri說(shuō)出這么一句話 Siri,在微信上告訴我的家人們今天我不回去吃飯了:
class LXDIntentHandler: INExtension {
override func handler(for intent: INIntent) -> AnyObject? {
if intent is INSendMessageIntent {
return LXDSendMessageIntentHandler()
}
// 這里可以判斷更多類型來(lái)返回
return nil
}
}
通過(guò)判斷intent事件是發(fā)送消息的聊天事件后,筆者創(chuàng)建了一個(gè)用來(lái)處理事件的LXDSendMessageIntentHandler類對(duì)象,并且返回。在對(duì)象創(chuàng)建完成之后需要完成Resolve、Confirm和Handle三個(gè)步驟,具體操作需要子類遵循實(shí)現(xiàn)INSendMessageIntentHandling協(xié)議來(lái)完成:
-
Resolve階段
這個(gè)階段需要我們找到消息的具體接收者。在這個(gè)過(guò)程中,可能會(huì)出現(xiàn)三種情況:Exactly one match、Two or more matches以及No matches,對(duì)于這三種情況的處理分別如下: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: //兩個(gè)或更多匹配結(jié)果 let disambiguations = matches.map { $0.inPerson() } resolutionResults.append(INPersonResolutionResult.disambiguation(with: disambiguations)) break case 1: //一個(gè)匹配結(jié)果 let recipient = matches[0].inPerson() resolutionResults.append(INPersonResolutionResult.success(with: recipient)) break case 0: //無(wú)匹配結(jié)果 resolutionResults.append(INPersonResolutionResult.unsupported(with: .none)) break default: break } } completion(resolutionResults) } else { //未從用戶語(yǔ)音中提取到信息,需要向用戶征詢更多關(guān)鍵信息 completion([INPersonResolutionResult.needsValue()]) } }上面的代碼用來(lái)確認(rèn)出消息中的
我的家人們指代的是哪些人,其中每個(gè)聯(lián)系人最終用一個(gè)INPerson的對(duì)象來(lái)表示。接著我們需要匹配消息的內(nèi)容:
func resolveContent(forSendMessage intent: INSendMessageIntent, with completion: (INStringResolutionResult) -> Void) {
if let text = intent.content where !text.isEmpty {
completion(INStringResolutionResult.success(with: text))
} else {
//向用戶征詢發(fā)送的消息內(nèi)容
completion(INStringResolutionResult.needsValue())
}
}
在匹配完消息接收者跟消息內(nèi)容之后,對(duì)于intent事件的處理就會(huì)進(jìn)入第二階段Confirm確認(rèn)值是否正確 Confirm階段
在這個(gè)階段intent對(duì)象本身的信息預(yù)計(jì)是已經(jīng)完成填充的,我們通過(guò)獲取這些填充值來(lái)判斷是否符合我們的要求。同時(shí)在這個(gè)階段,Siri會(huì)嘗試喚醒應(yīng)用來(lái)準(zhǔn)備完成最后的處理操作。前面說(shuō)了為了保證在應(yīng)用和應(yīng)用拓展之間能夠進(jìn)行通信,最好使用frameworks的方式來(lái)標(biāo)記應(yīng)用是否被啟動(dòng),再進(jìn)行相應(yīng)操作。
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事件中傳遞的數(shù)值的時(shí)候。要記住一點(diǎn),完全精確的內(nèi)容固然是最好的答案,但是過(guò)多的讓Siri詢問(wèn)用戶參數(shù)的詳細(xì)信息也會(huì)導(dǎo)致用戶的抵觸Handle階段
Handle階段不需要做太多額外的工作,判斷一下消息接收者或消息內(nèi)容是否存在,如果存在,執(zhí)行類似保存/發(fā)送的工作,然后完成。否則告訴Siri本次的intent事件處理處理失敗,我們還可以通過(guò)配置NSUserActivity對(duì)象來(lái)告訴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交互的過(guò)程中,我們同樣可以讓Siri展示響應(yīng)的自定義界面:

在我們創(chuàng)建
Intents Extension的時(shí)候,同時(shí)Xcode也詢問(wèn)我們是否創(chuàng)建Intents UI Extension。在后者的文件目錄下也有一個(gè)Info.plist,有著跟前面類似的鍵值對(duì),差別在于后者只有一個(gè)狀態(tài)的設(shè)置。
在這個(gè)文件目錄下存在一個(gè)故事板
MainInterface,這個(gè)故事板就是Siri和應(yīng)用交互時(shí)展示給用戶看的界面。通過(guò)修改這個(gè)故事板的界面元素,就可以實(shí)現(xiàn)上圖中的效果了。此外,在這個(gè)界面將要展示之前,我們可以修改類文件中的代碼完成界面信息填充的操作:
func configure(with interaction: INInteraction!, context: INUIHostedViewContext, completion: ((CGSize) -> Void)!) {
//這里執(zhí)行界面設(shè)置的代碼,完成之后執(zhí)行completion代碼就會(huì)讓界面展示出來(lái)
if let completion = completion {
completion(self.desiredSize)
}
}
var desiredSize: CGSize {
return self.extensionContext!.hostedViewMaximumAllowedSize
}
尾言
在觀看WWDC2016的新特性的時(shí)候,最開始給Siri和應(yīng)用的交互驚艷到了。但是后來(lái)閱讀文檔發(fā)現(xiàn)這種交互仍然存在著過(guò)多的限制,整體而言并沒(méi)有對(duì)Siri的使用帶來(lái)更明顯的提升。但是毫無(wú)疑問(wèn),這種交互如果蘋果能繼續(xù)對(duì)其進(jìn)行補(bǔ)充發(fā)展,可以給我們的應(yīng)用帶來(lái)更多的新活力。
文集:iOS開發(fā)