[iOS 10] SiriKit サンプルアプリ(UnicornChat)を動かしてみた(その2)
1 はじめに
前段に引き続き、Appleで公開されているSiriKitのサンプル(UnicornChat)を実行してみて、SiriKitの実装について概観してみました。
UnicornChat: Extending Your Apps with SiriKit
2016/09/11現在、上記のサンプルアプリは削除されています。(この他の多くのサンプルも、GM版公開時点で削除されています) 本記事は、削除される前(2016/08/01)のものを使用しております。 新しい、サンプルコードが公開されましたら、それを元に本記事は修正される予定です。
前段の記事は、下記です。
[iOS 10] SiriKit サンプルアプリ(UnicornChat)を動かしてみた(その1)
2 アプリ名の変更
Siriはアプリの名前を、(インテント拡張では無く、アプリ本体の方の)DisplayNameで認識しているようです。サンプルでは、ここが「$(PRODUCT_NAME)」となっており、UnicornChatと認識されているのです。
UnicornChatを一旦削除し、試験的に、ここを「柿の種」に変更して、インストールしてみました。
インストール完了後、「柿の種でメッセージ送って」と話しかけると、Siriは、「柿の種」をアプリ名と認識してくれているようです。
3 INExtension
インテント拡張は、INExtensionを継承しています。 INExtensionは、唯一のメソッドとしてハンドラを返していますが、デフォルトでは自分自身を返しています。
UnicornChatサンプルでは、UCSendMessageIntentHandlerという独自クラスを定義して、これをハンドラとして返しています。
// UCIntentsHandler.swift class UCIntentsHandler: INExtension { override func handler(for intent: INIntent) -> Any? { if intent is INSendMessageIntent { return UCSendMessageIntentHandler() } return nil } } //UCSendMessageIntentHandler.swift class UCSendMessageIntentHandler: NSObject, INSendMessageIntentHandling { // ・・・省略・・・ }
4 INSendMessageIntentHandling
Siriとの通信では、下記の3つのフェーズがありますが、それぞれの場面で当該メソッドが呼び出されることになります。
- Resolve (Intentのパラメータのバリデーションを行う)
- Confirm (Intentに対するレスポンスを生成してタスク実行の最終確認を行う)
- Handle (タスクの実行して結果を返す)
INSendMessageIntent(メッセージ送信インテント)でSiriと通信するハンドラは、INSendMessageIntentHandlingプロトコルを実装しなければなりません。
そして、INSendMessageIntentHandlingプロトコルには、次のようなメソッドが定義されています。
public protocol INSendMessageIntentHandling : NSObjectProtocol { // Handlerフェーズで呼ばれる public func handle(sendMessage intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Swift.Void) // Confirmフェーズで呼ばれる optional public func confirm(sendMessage intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Swift.Void) // Resolveフェーズ(宛先の確認)で呼ばれる optional public func resolveRecipients(forSendMessage intent: INSendMessageIntent, with completion: @escaping ([INPersonResolutionResult]) -> Swift.Void) // Resolveフェーズ(メッセージ本文の確認)で呼ばれる optional public func resolveContent(forSendMessage intent: INSendMessageIntent, with completion: @escaping (INStringResolutionResult) -> Swift.Void) // Resolveフェーズ(グループ名の確認)で呼ばれる optional public func resolveGroupName(forSendMessage intent: INSendMessageIntent, with completion: @escaping (INStringResolutionResult) -> Swift.Void) }
定義を見ると分かるように、handle(sendMessage:completion:)以外は、optionalになっていますが、UnicornChatサンプルでは、handle(sendMessage:completion:)、confirm(sendMessage:completion:)、resolveRecipients(forSendMessage:with:)、resolveContent(forSendMessage:with:)の4つが定義されていました。
(1) resolveRecipients
resolveRecipients(forSendMessage:with:)は、Resolveフェーズで宛先を確認する際に、呼ばれます。
func resolveRecipients(forSendMessage intent: INSendMessageIntent, with completion: @escaping([INPersonResolutionResult]) -> Swift.Void) { if let recipients = intent.recipients { var resolutionResults = [INPersonResolutionResult]() for recipient in recipients { let matchingContacts = UCAddressBookManager().contacts(matchingName: recipient.displayName) switch matchingContacts.count { case 2 ... Int.max: // We need Siri's help to ask user to pick one from the matches. let disambiguationOptions: [INPerson] = matchingContacts.map { contact in return contact.inPerson() } resolutionResults += [.disambiguation(with: disambiguationOptions)] case 1: let recipientMatched = matchingContacts[0].inPerson() resolutionResults += [.success(with: recipientMatched)] case 0: resolutionResults += [.unsupported()] default: break } } completion(resolutionResults) } else { // No recipients are provided. We need to prompt for a value. completion([INPersonResolutionResult.needsValue()]) } }
ここで、いったん、Siriに対して宛先を添えて指示をしてみます。「柿の種で太郎さんにメッセージ送って」
すると、「"柿の種"では”太郎さん”という連絡先は見つかりませんでした。」という返答が返ってきます。
UnicornChatサンプルでは、宛先が認識できなくても、resolveContent(forSendMessage:with:)で本文が取得できれば、INStringResolutionResult.successを返すので、宛先は必須では無いのですが、もし、"太郎さん"を宛先として認識させるのであれば、次のような方法があります。
resolveRecipients(forSendMessage:with:)では、intent.recipientsに、Siriが受け取った宛先のリストが入っており、これが有効な宛先であるかどうかをSiriに返すことが出来ます。
サンプルでは、連絡先の中に「太郎」が無くても、UniCornChatのコンタクトリストに追加することで、有効な宛先と判断させることが出来ます。 ※Siri開発者の一覧に名前を追加するのは、ちょっと気が引けますが・・・
UCAddressBookManager.m
- (NSArray<UCContact *> *)allContacts { UCContact *contact1 = [[UCContact alloc] init]; [contact1 setName:@"Bill James"]; [contact1 setUnicornName:@"Sparkle Sparkly"]; UCContact *contact2 = [[UCContact alloc] init]; [contact2 setName:@"Tom Clark"]; [contact2 setUnicornName:@"Celestra"]; UCContact *contact3 = [[UCContact alloc] init]; [contact3 setName:@"Juan Chavez"]; [contact3 setUnicornName:@"Dandelion Prince"]; UCContact *contact4 = [[UCContact alloc] init]; [contact4 setName:@"Anne Johnson"]; [contact4 setUnicornName:@"Pinky Nose"]; UCContact *contact5 = [[UCContact alloc] init]; [contact4 setName:@"太郎"]; [contact4 setUnicornName:@"太郎"]; NSArray<UCContact *> *allContacts = @[contact1, contact2, contact3, contact4, contact5, ]; return allContacts; }
上記の追加で「太郎」を宛先として認識させることが出来ました。
(2) resolveContent
resolveContent(forSendMessage:with:)は、Resolveフェーズで本文を確認する際に、呼ばれます。
func resolveContent(forSendMessage intent: INSendMessageIntent, with completion: @escaping(INStringResolutionResult) -> Swift.Void) { if let text = intent.content, !text.isEmpty { completion(INStringResolutionResult.success(with: text)) } else { completion(INStringResolutionResult.needsValue()) } }
次のようにメッセージの本文を(何が入っていても)「こんばんは」改変してみます。
func resolveContent(forSendMessage intent: INSendMessageIntent, with completion: @escaping(INStringResolutionResult) -> Swift.Void) { if let text = intent.content, !text.isEmpty { //completion(INStringResolutionResult.success(with: text)) completion(INStringResolutionResult.success(with: "こんばんは")) } else { completion(INStringResolutionResult.needsValue()) } }
実行結果は、次のとおりです。
(3) Confirm
confirm(sendMessage:completion:)は、Confirm(確認)フェーズで呼び出されるメソッドですが、実は、これは、Resolveフェーズのメソッドで、INStringResolutionResult.successが返された時点で直ちに呼ばれます。
// MARK: 2. Confirm func confirm(sendMessage intent: INSendMessageIntent, completion: @escaping(INSendMessageIntentResponse) -> Swift.Void) { if UCAccount.shared().hasValidAuthentication { completion(INSendMessageIntentResponse(code: .success, userActivity: nil)) } else { // Creating our own user activity to include error information. let userActivity = NSUserActivity(activityType: String(INSendMessageIntent.self)) userActivity.userInfo = [NSString(string: "error"):NSString(string: "UserLoggedOut")] completion(INSendMessageIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity)) } }
confirm(sendMessage:completion:)では、アプリとしてインテントに含まれる各データが、適正であるかを確認してSiriに伝えます。 サンプルにおけるUCAccount,shared().hasValidAuthenticationは、常にtrueが返るように実装されているため、ここは、無条件に.successが返ることになります。
パラメータであるintentには、取得できた、宛先(recipients[0].fullName)や、本文(content.values[0].value)に認識された項目が入っています。
この辺のデータに問題がないかのロジックは、ここで実装することが可能です。
(4) Handle
// MARK: 3. Handle func handle(sendMessage intent: INSendMessageIntent, completion: (INSendMessageIntentResponse) -> Swift.Void) { if intent.recipients != nil && intent.content != nil { // Send the message. let success = UCAccount.shared().sendMessage(intent.content, toRecipients: intent.recipients) completion(INSendMessageIntentResponse(code: success ? .success : .failure, userActivity: nil)) } else { completion(INSendMessageIntentResponse(code: .failure, userActivity: nil)) } }
確認画面で「送信」ボタンを押すと、処理中の画面となり、この時handleが呼ばれます。
このhandle(sendMessage:completion:)でintentを使用して、実際の処理(メッセージ送信)を行います。 UnicornChatサンプルでは、intentの宛先、及び本文を使用してsendMessage()でメッセージを送信するようなコードになっていますが、実際には、特に処理されていません。
- (BOOL)sendMessage:(NSString *)message toRecipients:(NSArray *)recipients { // Sending a message here... return YES; }
5 最後に
今回は、SiriKitのサンプルであるUnicornChatを動作させて見て、実装の要領を概観してみました。
ドキュメントにも有りましたが、Siriは、Extensionを認識するのに数分かかることがあります。作業中、動作がおかしいなと感じても、焦らず、何回か試すとうまくいくことが多くありました。 デバッグは、焦らず、少し待つのが大事かもしれません。
6 参考リンク
[iOS 10] SiriKit サンプルアプリ(UnicornChat)を動かしてみた(その1)
Siri + Apps
SiriKit Programming Guide
Introducing SiriKit
Extending Your Apps with SiriKit