iOS開發-SiriKit應用

關于SiriKit

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

Siri服務

iOS10之后,蘋果希望Siri能夠給用戶帶來更多的功能體驗,基于這個出發點,新增了SiriKit框架。Siri通過語言處理系統對用戶發出的對話請求進行解析之后生成一個用來描述對話內容的Intents事件,然后通過SiriKit框架分發給集成框架的應用程序以此來獲取應用的內容,比如完成類似通過文字匹配查找應用聊天記錄、聊天對象的功能,此外它還支持為用戶使用蘋果地圖時提供應用內置服務等功能。通過官方文檔我們可以看到SiriKit框架支持的六類服務分別是:

  • 語音和視頻通話
  • 發送消息
  • 收款或者付款
  • 圖片搜索
  • 管理鍛煉
  • 行程預約

SiriMaps通過Intents extension的擴展方式和我們的應用進行交互,其中,類型為INExtension的對象扮演著Intents extension擴展中直接協同Siri對象共同響應用戶請求的關鍵角色。當我們實現了Intents extension擴展并產生了一個Siri請求事件時,一個典型的Intent事件的處理過程中總共有這三個步驟Resolve、ConfirmHandle

  • Resolve階段。在Siri獲取到用戶的語音輸入之后,生成一個INIntent對象,將語音中的關鍵信息提取出來并且填充對應的屬性。這個對象在稍后會傳遞給我們設置好的INExtension子類對象進行處理,根據子類遵循的不同服務protocol來選擇不同的解決方案

  • Confirm階段。在上一個階段通過handler(for intent:)返回了處理intent的對象,此階段會依次調用confirm打頭的實例方法來判斷Siri填充的信息是否完成。匹配的判斷結果包括Exactly one matchTwo 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一并創建了,我們同樣選是。這樣我們在項目下面總共創建了LXDSiriExtensionLXDSiriExtensionUI兩個TARGET,這兩個文件目錄下面分別存在著一個新的info.plist文件,這個文件用來設置intent事件發生時我們設置的處理類。這里借用WWDC在講解時的一張ppt來了解:

按圖中的層次展開,IntentsSupportedIntentsRestrictedWhileLocked分別是兩個字符串數組,每一個字符串表示的是應用擴展處理的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類對象,并且返回。在對象創建完成之后需要完成ResolveConfirmHandle三個步驟,具體操作需要子類遵循實現INSendMessageIntentHandling協議來完成:

  • Resolve階段
    這個階段需要我們找到消息的具體接收者。在這個過程中,可能會出現三種情況:Exactly one matchTwo 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開發

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

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