freeeのiOSアプリをSiri Shortcutsに対応させた話

こんにちは。モバイルエンジニアの高野です。

iOS 12がリリースされましたね!様々なアップデートがありますが、とりわけ目立って幅広いアプリで活用できそうなのはやはりSiri Shortcutsではないでしょうか。

ということで、弊社のiOS版クラウド会計freeeでも早速Siri Shortcutsに対応したバージョンを本日リリースしました!

Developers blogですので今回はどのようにこの機能を開発していったかを紹介します。

どんな機能?

Siriに「口座を同期して」と話しかけて口座の同期を開始する様子

弊社のプロダクトには、クレジットカードや銀行口座の明細をAPIなどから取得する同期機能が備わっており、定期的にバックグラウンドで明細取得されます。しかし、任意のタイミングで明細を取得して、最新の状態で会計処理をしたい時もしばしばあります。

今回はこの機能をSiri Shortcutsに対応させて、好きなタイミングでSiriを使って簡単に明細取得をできるようにしました。

Siriに「口座を同期して」と話しかけると、登録してあるクレジットカード・銀行口座の同期を開始し、新しい明細の取得が完了したらPush通知で知らせてくれる、という体験になります。

もちろん、Shortcutに対応させたことで、Siriに話しかける方法だけではなく、Spotlightのサジェスト・場合によってはロックスクリーンにもShortcutが表示される(ロックスクリーンに表示される条件はブラックボックスですが)ことになります。

WWDCのセッションの中で、Shortcutとして提供する機能はアプリ内で繰り返し行われる重要な機能が適していると言及されていましたが、今回それにピッタリの題材になったかなと思います。

Siri Shortcutsに対応させるために行うこと

ここから、今回の機能のために必要だった作業の流れを紹介していきます。

まずSiri Shortcutsに対応させるための開発には、SiriKitを利用します。SiriKitにはIntents frameworkとIntents UI frameworkが含まれています。

対応するにあたって参照したドキュメントは以下です。
SiriKit | Apple Developer Documentation

また、Appleが提供するサンプルコードであるSoup Chefも参照しました。
Soup Chef: Accelerating App Interactions with Shortcuts | Apple Developer Documentation

Intentの定義

さて、ではまずはIntentを定義します。IntentというのはSiriが受け付けられるリクエストのことを指します。

今回は「口座を一括同期する」と言うIntentが必要なわけですが、システムが標準で提供する特定ドメイン(MessagingやPayment等)のIntentとは合致しないため、カスタムインテントを作っていきます。

Intent Definition fileという新たに追加された形式のファイルを使って、Intentのタイトルやサブタイトル、パラメータ、リクエストに対してのレスポンスを定義していきます。このファイルはXcode 10~の「File > New > SiriKit Intent Definition File」から作成できます。

Intent Definitionを作成している様子のスクリーンショット

今回の定義は非常にシンプルで、タイトルや説明文などを記述しているだけです。CategoryにStart・Do・Run・Goなど汎用的な物が指定できるため、幅広いアプリでSiri Shortcutに対応させることができますね。

Intent Definition fileでIntentの内容を定義してからビルドすると、Intent Definition fileの内容に基づいたprotocolやclassを定義するコードが自動生成されます。 このコードのコンフリクトを避けるため、Intent Definition fileを開いた状態にし、XcodeインスペクタのTarget Membershipパネルで

  • 共有Framework(詳しくは後述します)の設定を”Intent Classes”
  • App、App Extensionの設定は”No Generated Classes”

と設定します。そうすることで、共有Frameworkに向けてのみコード生成されるようになり、コンフリクトが避けられます。

Intents App Extension

次に、Siri経由のリクエストをハンドリングするためのApp Extensionを用意します。

ユーザーからのリクエストに対して常にアプリを起動して反応する場合、このApp Extensionを作る必要はありません。ですが、ユーザーのリクエストに反応して何らかの処理をバックグラウンドで行いたい場合は、このIntents App Extensionを用意する必要があります。
バックグラウンドで処理を完了できれば、ユーザーはそれまで表示していた画面から離れることなくアクションを実行でき、より良いユーザー体験を提供することができるため、基本的にはIntents App Extensionを用いてバックグランドでタスクを完了できるようにした方がいいのではないかと思います。

Intents App Extensionの役割の大部分は、Siriとアプリが提供するShortcutを機能させるロジックとの橋渡しになります。今回私が実装した内容も、部分的に省略していますがだいたい以下のようなレベルの簡素な物です。

import Intents
import OurPrivateFramework

class IntentHandler: INExtension, FreeeFetchWalletableTransactionsIntentHandling {
  override func handler(for intent: INIntent) -> Any {
    return self
  }
  
  func handle(intent: FreeeFetchWalletableTransactionsIntent, completion: @escaping (FreeeFetchWalletableTransactionsIntentResponse) -> Void) {
    // 口座同期を開始するAPIにリクエストを送る
    APIClient.callTheAPI { success in
      completion(FreeeFetchWalletableTransactionsIntentResponse(code: success ? .success : .failure, userActivity: nil))
    }
  }
}

Siriからの要求をハンドリングするメソッドが、自動生成されたprotocolで定義されているので、そのメソッドの中で共有Frameworkに実装されたロジックを実行する感じです。
前述の、Intent Definition fileのスクリーンショットに「User confirmation required」という項目がありますが、Shortcutの実行前にユーザーに確認を促すべきアクションなどの場合はこれにチェックを入れ、IntentHandlerでconfirmするメソッドを実装する形になります。

注意点として、UIApplicationDelegateapplication(_:continue:restorationHandler:)はIntents App Extensionを提供しないアプリにおいてSiri経由のリクエストハンドリングする場合にも呼び出されるAPIですが、仮にIntents App Extensionを用意していたとしても、バックグラウンドでの処理ができなかった場合などに対応するため、application(_:continue:restorationHandler:)は実装するように、とドキュメントに書かれています。

Shared Framework

次に、AppとApp Extensionの間で共有する機能をこの共有Frameworkに実装します。「Intentの定義」のところで少し触れた共有Frameworkのことですね。Intent Definition fileを元に自動生成されるコードも、前述したTarget Membershipの設定を変更し、このFrameworkに含まれるようにします。

弊社のアプリでは、Siriから実行するための口座同期を開始するAPIの定義などがこのFrameworkに含まれています。

Siri対応に限らずApp Extensionを提供する際にはいずれにしろこのようなFrameworkが必要になることが多いですし、仮にApp Extensionに対応していないアプリでも、サービスのコアとなる機能をこのレイヤーに切り出しておくと設計としても良いのではないかと思います。

ShortcutのDonation

最後の仕上げとして、Shortcutとして提供したい機能をSiriに提供します。アプリ内でSiri Shortcutsに対応させる機能が利用される度にDonationするコードを実行します。ドキュメントによると、この時の時刻や位置等の環境を元にSiriがサジェストしてくれるようになるということです。

ただし、Siri経由でその機能が実行された場合は、システムは暗黙的にそのことを認知するようで、明示的にDonationする必要なありません。
逆にユーザーが一度も行ったことのないアクションを提供はしてはならず、将来的にユーザーが行う可能性の高いアクションについてはRelevant ShortcutsとしてSiriに提供するようにと書かれています。

アクションは、NSUserActivityINInteractionを使ってSiriに提供できますが、今回はINInteractionを使って、以下のようなコードでDonationしました。

let intent = FreeeFetchWalletableTransactionsIntent()
INInteraction(intent: intent, response: nil).donate { _ in }

エラー処理など省略していますが、Donationするのに最低限必要なコードはこれだけです。
これを、アプリ内で口座の同期ボタンがタップされる際に実行しておくようなイメージです。

Add to Siri

さて、ここまで行えばSiri Shortcutsには対応できていて、SpotlightやロックスクリーンへのShortcut表示は行われる下地が整った状態になります。 ここで更にSiriらしく、アプリ内で任意の音声フレーズを今回作成したShortcutに割り当て、音声でShortcutを実行できるようにする方法を紹介します。

まずはIntents UI frameworkに含まれるINUIAddVoiceShortcutButtonを使って、Add to Siriボタンを画面に表示します。
INUIAddVoiceShortcutButtonのイニシャライザは、enumとして定義されるINUIAddVoiceShortcutButtonStyleを引数として受け、それに応じてボタンの外観を変更します。現時点ではblack, blackOutline, white, whiteOutlineが定義されており、その中からアプリに合う物を選ぶことになります。

let button = INUIAddVoiceShortcutButton(style: .white)
view.addSubView(button)

表示はこのようになります。

Add to Siriボタンのスクリーンショット(白背景と黒背景)

INUIAddVoiceShortcutButtonのdelegateを設定しておくと、このボタンがタップされた時にdelegateのpresent(_:for:)メソッドが呼ばれます。その引数に渡されるINUIAddVoiceShortcutViewControllerを表示することで、フレーズを登録する画面がをユーザーに提供することができます。すでにフレーズが登録されている場合は自動的にボタンのラベルが変化し、ボタンをタップすると、編集画面を起動するためのpresent(_:for:)メソッドが呼ばれます。

現時点でINUIAddVoiceShortcutButtonのリファレンスに載っているサンプルコードだと、ボタンにaddTargetで設定したセレクタでINUIAddVoiceShortcutViewControllerインスタンスを作って画面に表示している箇所があり、この理由は分かりませんが、delegateを使ったほうがシンプルに書けると思います。

まとめ

大まかにではありますが、会計freeeでSiri Shortcutsに対応した際の流れを紹介しました。

弊社のモバイルチームではHack dayという、いわゆる20%ルール的な制度を設け、その中でエンジニアが自律的に作りたいものを作っていけるようにしています。実は今回のSiri Shortcuts対応はその中から生まれた機能です。この制度を使ってこれまでに色々な機能のリリースや、開発基盤の整備などが行われています。

ということで、freeeのモバイルチームではやっていきによってfreeeのプロダクトをもっとよくしてくれる仲間を募集しています!

スモールビジネスの未来をアプリで切り拓くモバイルエンジニア募集! - freee 株式会社のモバイルエンジニア中途の求人 - Wantedly

jobs.freee.co.jp