iOS開發(fā)-SiriKit應(yīng)用

關(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)容

Siri服務(wù)

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ù)約

SiriMaps通過(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、ConfirmHandle

  • 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 matchTwo 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)建了LXDSiriExtensionLXDSiriExtensionUI兩個(gè)TARGET,這兩個(gè)文件目錄下面分別存在著一個(gè)新的info.plist文件,這個(gè)文件用來(lái)設(shè)置intent事件發(fā)生時(shí)我們?cè)O(shè)置的處理類。這里借用WWDC在講解時(shí)的一張ppt來(lái)了解:

按圖中的層次展開,IntentsSupportedIntentsRestrictedWhileLocked分別是兩個(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事件的類。
plist設(shè)置

另外,官方講解中提到了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、ConfirmHandle三個(gè)步驟,具體操作需要子類遵循實(shí)現(xiàn)INSendMessageIntentHandling協(xié)議來(lái)完成:

  • Resolve階段
    這個(gè)階段需要我們找到消息的具體接收者。在這個(gè)過(guò)程中,可能會(huì)出現(xiàn)三種情況:Exactly one matchTwo 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ā)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • WWDC2016,Apple發(fā)布了ios10,每次版本發(fā)布,都會(huì)帶來(lái)新的接口,新的機(jī)會(huì),也能間接的看到Apple的...
    elarc閱讀 3,282評(píng)論 0 2
  • 一、SiriKit介紹 Siri是一款蘋果 iOS 系統(tǒng)提供的智能語(yǔ)音助手軟件,它的全名是 Speech Inte...
    火星抄手閱讀 5,897評(píng)論 15 19
  • 概覽 最新的WWDC2016大會(huì)上,蘋果提出iOS10并推出了十個(gè)新特性,homekit、messageapp等等...
    cuagain閱讀 2,364評(píng)論 0 5
  • 隨著iOS10.0發(fā)布腳步的臨近,作為開發(fā)者,相信很多人也和我一樣,可以提前體驗(yàn)一些新系統(tǒng)的新功能,也更關(guān)注新版i...
    MarkCJ閱讀 19,865評(píng)論 0 18
  • 1.原文地址2.Additional Framework Changes章節(jié)還沒(méi)來(lái)得及翻譯,之后會(huì)出3.有些不適合...
    subvertWuxu閱讀 6,530評(píng)論 2 39

友情鏈接更多精彩內(nèi)容