この記事は公開されてから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を一旦削除し、試験的に、ここを「柿の種」に変更して、インストールしてみました。
インストール完了後、「柿の種でメッセージ送って」と話しかけると、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