先日のWWDC18で、iOS 12の新機能 "Siri Shortcuts"が発表されました。
「よくやる手順を声で呼び出せる」というのはもちろん嬉しいことですが、何よりも、
「ロックスクリーンから呼び出せる」
というのは、プラットフォームの制約の中で取捨選択するしかないいちアプリ開発者にとっては圧倒的魅力に感じます。
「何がどこまでできるのか、どう実装するのか」を掴むべく、まずはその最小実装を確認してみました。
ちなみに参考資料はWWDC 2018のセッション番号211「Introduction to Siri Shortcuts」とサンプルコード「SoupChef」です。1
実装方法は2通り
Siri Shortcutsの実装方法としては、以下の2種類があります。
- NSUserActivityを利用する方法
- Intentsを利用する方法
本記事では、タイトル通り、NSUserActivity
を用いたSiri Shortcutsの最小実装だけを紹介します。
Appleのサンプル「SoupChef」は両方の実装が入っており、それはそれでありがたいのですが、初めて挑む人にはどのコードがどっち用なのか、たとえばIntentsを使わない場合はどれを省けるのかといったことがわかりづらいと思うので、そのあたりを本記事で紐解ければと。
3ステップ
NSUserActivity
を用いたSiri Shortcutsの実装は、次の3つの手順で行います。
- ショートカットを定義する
- ショートカットを提供する(donate) 2
- ショートカットをハンドルする
「アプリの画面Bを開く」というショートカットをSiri Shortcutsを実現してみます。
1. ショートカットを定義する
Info.plistに次のようにNSUserActivityTypes
を定義します。
<key>NSUserActivityTypes</key>
<array>
<string>com.myapp.name.my-activity-type</string>
</array>
2. ショートカットを提供する
Intents
をインポートして、
import Intents
次のようにNSUserActivity
を用意します。
extension NSUserActivity {
public static let myActivityType = "com.myapp.name.my-activity-type"
public static var myActivity: NSUserActivity {
let userActivity = NSUserActivity(activityType: myActivityType)
userActivity.isEligibleForSearch = true
userActivity.isEligibleForPrediction = true
userActivity.title = "My First Activity"
userActivity.suggestedInvocationPhrase = "Let's do it"
return userActivity
}
}
一見複雑に見えますが、Siri Shortcutsに関係するポイントとしては、
-
isEligibleForPrediction
プロパティにtrue
をセットする -
suggestedInvocationPhrase
プロパティをセットする- ここにセットしたフレーズがショートカットの音声コマンドをユーザーに録音してもらう画面でサジェストされる
これぐらいです。
Appleのサンプルには、次のようにCSSearchableItemAttributeSet
を作成してcontentAttributeSet
にセットする実装も入っていますが、
let attributes = CSSearchableItemAttributeSet(itemContentType: "hoge")
attributes.thumbnailData = UIImage(named: "filename")!.pngData()
attributes.keywords = ["foo", "bar"]
attributes.displayName = "My First Activity"
attributes.contentDescription = "Subtitle"
userActivity.contentAttributeSet = attributes
私が試したところではSiri Shortcutsを行うだけであれば省略可能でした3。検索窓(Spotlight)からのテキストによる検索にも対応させたい場合に必要なのではないでしょうか。
作成したNSUserActivity
オブジェクトを、ショートカットを提供するUIViewControlller
のuserActivity
プロパティ(正確にはUIResponder
のプロパティ)にセットします。
userActivity = NSUserActivity.myActivity
ここでもAppleのサンプルでは次のようにupdateUserActivityState(_)
をオーバーライドしてNSUserActivity
のaddUserInfoEntries(from:)
メソッドでuserInfo
のデータを供給する実装が入っていましたが、これもなくても動作しました。
override func updateUserActivityState(_ activity: NSUserActivity) {
let userInfo: [String: Any] = [NSUserActivity.ActivityKeys.menuItems: menuItems.map { $0.itemNameKey },
NSUserActivity.ActivityKeys.segueId: "Soup Menu"]
activity.addUserInfoEntries(from: userInfo)
}
ショートカットでアプリが起動する際に、アプリの状態を復元するために必要なデータ(userInfo
)がある場合に実装すべきものと思われます。
💡NSUserActivity
やCSSearchableItemAttributeSet
が初見の方へ
手前味噌ですが、「iOS 9 の新機能のサンプルコード集」の"Search APIs"サンプルを実行してみつつコードを読んでみると非常にわかりやすいかと思います。NSUserActivity を使うものと、Core Spotlight を使うものの2種類が実装してあり、コードがシンプルなのでどのプロパティが何に対応してるのかが明確です。
3. ショートカットをハンドルする
ショートカットが呼び出されてアプリが起動する際、UIApplicationDelegate
のapplication(_:continue:restorationHandler:)
が呼び出されるので、そこで渡されてくるアクティビティ(NSUserActivity
)をハンドルします。
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if userActivity.activityType == NSUserActivity.myActivityType {
// Restore state for userActivity and userInfo
guard let window = window,
let rootViewController = window.rootViewController as? UINavigationController,
let vc = rootViewController.viewControllers.first as? ViewController else {
os_log("Failed to access ViewController.")
return false
}
vc.performSegue(withIdentifier: "B", sender: nil)
return true
}
return false
}
ここではactivityType
でどのアクティビティかを判定し、あとは愚直にperformSegue
で画面Bに遷移させているだけです。
完成品の挙動
NDA期間中のため完成品の挙動をキャプチャしてアップすることはしませんが、
- 設定から当該Siri Shortcutを登録
- ロックスクリーンから登録したフレーズで呼び出す
これでアプリが起動し、画面Bまで自動的に遷移しました。
アプリはバックグラウンドで生きている必要があるのか?
気になったのが、UIViewController
のuserActivity
プロパティに当該NSUserActivity
オブジェクトをセットした点です。これってこのView Controllerがショートカットのdonatorであり、アプリが生きてないと呼び出せないのか?と。
というわけでアプリをkillしてもNSUserActivity
ベースのSiri Shortcutは動作するか試してみました。
結果 → 動作しました。
良かったです。
次回
- Intentsを用いる場合の最小実装
- 両者をどう使い分けるか
- Siri Shortcutsで呼び出せない機能はたとえばどんなものがある?
といったあたりについて書きます。(書けたらいいなと思っています)
おまけ
Objective-Cのコードも書く機会があったので、残しておきます。
#import "NSUserActivity+SiriShortcuts.h"
@import Intents;
NSString *kMyActivityType = @"com.myapp.name.my-activity-type";
@implementation NSUserActivity (SiriShortcuts)
+ (NSUserActivity *)myActivity {
if (@available(iOS 12.0, *)) {
NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:kMyActivityType];
userActivity.eligibleForSearch = YES;
userActivity.eligibleForPrediction = YES;
userActivity.title = @"My Activity";
userActivity.suggestedInvocationPhrase = @"Let's do it";
return userActivity;
} else {
return nil;
}
}
@end