[iOS 10] SiriKit サンプルアプリ(UnicornChat)を動かしてみた(その2)

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

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を一旦削除し、試験的に、ここを「柿の種」に変更して、インストールしてみました。

014

015

インストール完了後、「柿の種でメッセージ送って」と話しかけると、Siriは、「柿の種」をアプリ名と認識してくれているようです。

016 017

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に対して宛先を添えて指示をしてみます。「柿の種で太郎さんにメッセージ送って」

すると、「"柿の種"では”太郎さん”という連絡先は見つかりませんでした。」という返答が返ってきます。

019

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;
}

上記の追加で「太郎」を宛先として認識させることが出来ました。

020

(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())
    }
}

実行結果は、次のとおりです。

021

(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