Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

iOS 13におけるSiri Shortcuts 最小実装+α スライド書き起こしと補足

こんにちは、モバイルクライアントグループの明渡です。

先日iOSDC Japan 2020にてLTへ登壇させていただき、「iOS 13におけるSiri Shortcuts 最小実装+α」というテーマで発表いたしました。

fortee.jp

フィードバッグも思いの外たくさん頂戴いたしまして、登壇してみてよかったなと嬉しい限りですし、励みになります。ありがとうございました!

こちらは、上記LTをテーマとした以下の内容を含む記事となっております。

  • スライドの内容書き起こし
  • 補足
    • 一部のフィードバックへの回答
    • 観測した一部のニコ生コメントへの補足
    • Ask the Speakerで回答した内容
    • スライド作成過程で削った発表内容

なお、タイトルでiOS 13におけるとうたっておりますが、iOS 13から14へのアップデート内容はこちらの記事の内容でカバーしてる範囲について特に影響ございません。

StudyplusのSiri Shortcuts

2020年6月23日、StudyplusでSiri Shortcuts機能をリリースしました🎉

Siri Shortcutsに対応した画面

2画面対応を行い、それぞれAppleがSiri Shortcutsの実装を勧めるアプリを利用する上で何回も繰り返し行われる操作に該当すると判断して実装しました。

  • ユーザーのQRコード画面
  • 書籍のバーコード読み取りカメラ画面

f:id:m_yamada1992:20201012144642j:plain

Siri Shortcuts 利用状況

※2020年6月23日〜9月13日時点

  • ショートカット経由でアプリを開いた
    • 回数:4,697
    • 人数:4,438
  • Add to Siriボタンからアプリを開くショートカットを登録した
    • 回数:7,942
    • 人数:4,889

補足: 集計方法

導入済みのFirebase Analyticsにて上記の集計対象の操作をイベントとして定義し、ユーザーが該当の操作をした際に送信してます。 送信タイミングはこの発表のスライドにて記載したコードの範囲に含まれております。

Siri Shortcuts 最小実装

実装方法の前提

Siriショートカット自体に実装方法は大きく二通りありますが、紹介するのはNSUserActivityを用いる方法のみです。

  • Intents & Intents UIを用いる
    • アプリにて繰り返し行う動作を様々な値を受け取りつつ再現可能
  • NSUserActivityを用いる
    • 特定の画面を開いた状態のアプリを起動するなど、単純な動作を再現可能

iOS 14のアップデートでIntents UI側は追加要素があったものの、NSUserActivityのみ用いる実装には影響ないです。

実装手順

1. Info.plistにアクションのIDを定義

<plist version="1.0">
<dict>
<!--  中略  -->
    <key>NSUserActivityTypes</key>
    <array>
        <string>$(BUNDLE_ID).materialBarcodeRead</string>
        <string>$(BUNDLE_ID).viewQRCode</string>
    </array>
</dict>
</plist>

2. 該当のアクションを行うNSUserActivityオブジェクトを生成

Info.plistへ定義したアクションIDを含む、NSUserActivityオブジェクトを生成します。

コードはStudyplusバーコード読み取りカメラ画面用のオブジェクト生成処理より引用しました。

import Intents

struct MaterialBarcodeReadAction {
    
    static var actionName: String {
        return "\(bundleId).materialBarcodeRead"
    }
    
    static var userActivity: NSUserActivity {
        let userActivity = NSUserActivity(activityType: actionName)
        userActivity.persistentIdentifier = actionName
        userActivity.title = "教材のバーコードを読み取る"
        return userActivity
    }
}

3. 該当のアクションで開くUIViewControllerへオブジェクトを設定

生成したNSUserActivityオブジェクトを、アクションの結果開きたい画面のuserActivityプロパティへ入れます。

final class MaterialBarcodeReaderViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let activity = MaterialBarcodeReadAction.userActivity
        userActivity = activity
    }
}

4. AppDelegateにハンドル時の処理を実装

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {

    switch userActivity.activityType {
    case MaterialBarcodeReadAction.actionName:
        // バーコード読み取りカメラ画面表示
        return true
    case ViewQRCodeAction.actionName:
        // QRコード画面表示
        return true
    default:
        return false
    }
}

最小実装を済ませるとできること

iOS 13以降はプリインストールされている"ショートカット"アプリにて、利用可能なアクションとして選択肢が表示されます。

※選択肢に表示されるには、Siri Shortcuts実装した後の対象画面を1回以上開いている必要はあります。

ショートカットの登録手順

"ショートカット"アプリ上で以下の操作を行うと選択肢に表示されています。

f:id:m_yamada1992:20201012174411j:plain

f:id:m_yamada1992:20201012174333j:plain

ギャラリータブからも追加する導線もあります。

f:id:m_yamada1992:20201012174338j:plain

Spotlightの“Siriからの提案”にショートカットを載せる

最小実装のみだとユーザーが見つけにくいので、導線を増やしましょう。

実装手順

1. NSUserActivityオブジェクトにて提案に載せる値を設定

NSUserActivityオブジェクトを生成時に、Spotligit向けの値の設定します。

#if canImport(CoreSpotlight)
import CoreSpotlight
import MobileCoreServices
#endif
/* 省略 */
    static var userActivity: NSUserActivity {
        let userActivity = NSUserActivity(activityType: actionName)
        userActivity.persistentIdentifier = actionName
        userActivity.title = "教材のバーコードを読み取る"

        userActivity.isEligibleForPrediction = true
        #if canImport(CoreSpotlight)
        let attributes = CSSearchableItemAttributeSet(itemContentType: kUTTypeContent as String)
        attributes.title = "教材のバーコードを読み取る"         
        attributes.contentDescription = "登録したい教材のバーコードを読み取れます"
        userActivity.contentAttributeSet = attributes
        #endif
        
        return userActivity
    }

isEligibleForPredictiontrueへ設定すると、Spotlightで"Siriからの提案"として表示を有効にします。 このプロパティをtrueにしないとSpotlightに永遠に表示されないので注意です。

2. 該当ViewControllerを開いた際に通知する

画面からOSへ、このアクションが実装されている操作をした旨を通知をします。

final class MaterialBarcodeReaderViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let activity = MaterialBarcodeReadAction.userActivity
        userActivity = activity

        activity.becomeCurrent()
    }
}

activity.becomeCurrent()が、実装済みのNSUserActivitiyオブジェクトで再現できる操作をしたことを通知する処理です。

通知の実装を済ませるとできること

該当の画面をよく使う場合、"Siriからの提案"へ表示されるようになります。

"Siriからの提案"で該当アクションを選択すると、アクションが実行され画面に遷移します。 この導線から、ショートカットの追加はできません。

"Siriからの提案"のデバッグについて

端末の設定 > デベロッパ > Display Recent ShortcutsをON にしましょう。

"該当の画面をよく使う場合"という定義が、具体的にどの程度の回数や条件を満たせば良いのかはアプリ開発者に公開されていません。動作確認時は必ずデバッグ機能を利用しましょう。

Add to Siriボタンを配置する

Add to Siriボタンを配置すると、実際にその画面をよく利用するユーザーが見つけやすくなります。

なお、Add to Siriボタンを配置するか否かの判断は、後述するAdd to Siriボタンを配置するとよい画面、配置しないほうがよい画面も踏まえることをお勧めします。

実装手順

1. ボタンを配置する

final class MaterialBarcodeReaderViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let activity = MaterialBarcodeReadAction.userActivity
        userActivity = activity

        activity.becomeCurrent()

        let addToSiriButton = INUIAddVoiceShortcutButton(style: .automaticOutline)
        addToSiriButton.shortcut = INShortcut(userActivity: activity)
        addToSiriButton.delegate = self
        
        view.addSubview(addToSiriButton)
    }
}

Add To Siriボタンのオブジェクトが上記におけるaddToSiriButtonです。 ショートカットへ登録させたいアクションのNSUserActivityオブジェクトを基に生成したINShortcutを、shortcutプロパティへ入れます。

また、INUIAddVoiceShortcutButtonDelegate に準拠して、次に実装を行うボタン押下時のイベントを受け取れるようにします。

2. ボタンを押下後の処理を実装

extension MaterialBarcodeReaderViewController: INUIAddVoiceShortcutButtonDelegate {
    
    func present(_ addVoiceShortcutViewController: INUIAddVoiceShortcutViewController, 
                 for addVoiceShortcutButton: INUIAddVoiceShortcutButton) {

        addVoiceShortcutViewController.delegate = self
        present(addVoiceShortcutViewController, animated: true, completion: nil)
    }
    
    func present(_ editVoiceShortcutViewController: INUIEditVoiceShortcutViewController,
                 for addVoiceShortcutButton: INUIAddVoiceShortcutButton) {

        editVoiceShortcutViewController.delegate = self
        present(editVoiceShortcutViewController, animated: true, completion: nil)
    }
}

INUIAddVoiceShortcutViewController、およびINUIEditVoiceShortcutViewController は親にUIViewControllerを持つSiriに追加画面専用クラスです。 上記コードのpresentは通常のUIViewControllerでモーダル表示を行う際の処理と本質的に変わりません。

補足: 親にUIViewControllerを持つためできることの例

親にUIViewControllerを持つSiriに追加画面専用クラス

なので、iOS 13以降の標準であるmodalPresentationStyleプロパティへ.pageSheetが設定済みのモーダル表示をした際に、下スワイプ操作で画面を閉じた場合に通常の画面と同様に検知する手段があります。

extension MaterialBarcodeReaderViewController: INUIAddVoiceShortcutButtonDelegate {
    
    func present(_ addVoiceShortcutViewController: INUIAddVoiceShortcutViewController, 
                 for addVoiceShortcutButton: INUIAddVoiceShortcutButton) {

        addVoiceShortcutViewController.delegate = self
        addVoiceShortcutViewController.presentationController?.delegate = self 

        present(addVoiceShortcutViewController, animated: true, completion: nil)
    }
    
    func present(_ editVoiceShortcutViewController: INUIEditVoiceShortcutViewController,
                 for addVoiceShortcutButton: INUIAddVoiceShortcutButton) {

        editVoiceShortcutViewController.delegate = self
        editVoiceShortcutViewController.presentationController?.delegate = self 

        present(editVoiceShortcutViewController, animated: true, completion: nil)
    }
}

表示元のクラスをUIAdaptivePresentationControllerDelegateへ準拠して、presentationController?.delegateプロパティに入れます。

extension MaterialBarcodeReaderViewController: UIAdaptivePresentationControllerDelegate {
    
    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        // 画面が閉じた際に行う処理
    }
}

通常のUIViewControllerと同様に、下スワイプにより画面を閉じる操作を検知して処理を実行できるようになります。

Studyplusでは、カメラ映像のキャプチャを伴うバーコード読み取り画面でSiriに追加画面を開く際にキャプチャを一時停止しております。 そしてSiriに追加画面でショートカットの追加・編集・削除・キャンセル時に加え、下スワイプ操作で画面を閉じた際にもキャプチャを再開する実装をしました。

3. Siri Shortcutsの追加・編集完了時の処理を実装

Siriに追加画面にてショートカット追加完了時
extension MaterialBarcodeReaderViewController: INUIAddVoiceShortcutViewControllerDelegate {

    func addVoiceShortcutViewController(
             _ controller: INUIAddVoiceShortcutViewController, 
             didFinishWith voiceShortcut: INVoiceShortcut?, error: Error?) {

        controller.dismiss(animated: true, completion: nil)
    }
    
    func addVoiceShortcutViewControllerDidCancel(
             _ controller: INUIAddVoiceShortcutViewController) {

        controller.dismiss(animated: true, completion: nil)
    }
}
Siriに追加画面にてショートカット編集・削除完了時
extension MaterialBarcodeReaderViewController: INUIEditVoiceShortcutViewControllerDelegate {
    
    func editVoiceShortcutViewController(
             _ controller: INUIEditVoiceShortcutViewController,
             didUpdate voiceShortcut: INVoiceShortcut?, error: Error?) {
        controller.dismiss(animated: true, completion: nil)
    }
    
    func editVoiceShortcutViewController(
             _ controller: INUIEditVoiceShortcutViewController, 
             didDeleteVoiceShortcutWithIdentifier deletedVoiceShortcutIdentifier: UUID) {
        controller.dismiss(animated: true, completion: nil)
    }
    
    func editVoiceShortcutViewControllerDidCancel(_ controller: INUIEditVoiceShortcutViewController) {
        controller.dismiss(animated: true, completion: nil)
    }
}

Add to Siriボタンを配置するとできること

"ショートカット"アプリを経由せず、自分のアプリからSiri Shortcutsを追加・編集・削除することが可能になります。

実装してみたうえでの私的考察

Add to Siriボタンを配置するとよい画面、配置しないほうがよい画面

Add to Siriボタンは、ショートカットを追加後も編集と削除の導線を担保し続けることが前提です。

それを踏まえると、それぞれ以下の通りです。

配置すると良い画面

  • 配置することで本来の役割を全うするのに支障が出ない
    • もともと余白が十分にあり、永続的に表示し続けても問題ない
    • クッションページとして別画面を挟む形で表示しても差し支えない

配置しないほうがよい画面

  • 配置すると本来の役割を全うするための領域が狭くなる画面
    • 様々な情報を見るための画面など

画面の役割を邪魔せず配置できる場合はメリットが勝るでしょう。 そうでない場合はデメリットが大きいので、ボタンの実装はやめておきましょう。

提案フレーズの選びかた

提案フレーズとは

NSUserActivityオブジェクトのsuggestedInvocationPhraseプロパティです。

Siriに追加画面から登録する際、呼びかけるフレーズのデフォルト値として表示されます。

f:id:m_yamada1992:20201012192427j:plain

そのまま呼びかけて不都合のないフレーズを設定しておくと、ユーザーは自分で「何て呼びかけよう?」と考える手間を省けて便利です。

補足: 提案フレーズの多言語対応

実際に動作検証はしておらず恐縮ですが、おそらく通常アプリ内で多言語対応した文言を呼び出す際に利用するNSLocalizedString(key, comment)では実現できません。 iOS 13時点での話ですが、Spotlightに表示する文言などはちょっと更新してビルドしたりアプリ再インストールしても即座に反映されなくて、検証に少し根気がいるんですよね...

AppleのSiri ShortcutsサンプルコードであるSoupChefにて、提案フレーズとIntents UIから参照する文言はNSString.deferredLocalizedIntentsString(with:table:arguments:) を利用しているので、そちらを試すとよさそうです。

よりユーザーが利用しやすいフレーズを選ぶには

3つに気をつけて選定してみましょう。

  • 他アプリのショートカットと重複しない
  • 検証する端末
  • 検証する環境
他アプリのショートカットと重複しない
  • 同じ端末内に共存している可能性が高いアプリのショートカットを確認
    • 類似のショートカットがある場合、共存しても呼び分けできるようアプリ名を含めるのがオススメ
    • アプリ名は優先して聞き取りしてくれる様子
    • アプリ名でも略称などは要検証

なお、StudyplusではTwitterにQRコードに関するショートカットが存在することを考慮して、QRコード画面を開く提案フレーズを「スタディプラスのQRコード」としました。

補足: 提案フレーズにアプリ名を含めることの是非

いただいたフィードバックにて、「ガイドラインに以下の記述があるので、アプリ名は含めない方がよいと思っていたのですがいかがでしょう?」とご質問をいただきました。

Exclude your app name. The system already identifies the app associated with a shortcut.

引用元:Shortcuts and Suggestions - Siri - Human Interface Guidelines - Apple Developer

私が実装調査時に主に確認したのはWWDCのSiri Shortcuts関連動画の方なのですが、あくまで「アプリを識別するのには不要だから無闇に含める必要はない」という文脈で語られていた認識です。 それを踏まえての私的解釈ですが、アプリ名を含めることでユーザーの利便性向上を見込めるなら入れても良いんじゃないかと思ってます。

現実的として、同じ端末内に共存する類似ショートカットを複数利用することがありうるならば、最初から呼び分けられる提案フレーズで登録できたほうが親切でしょう。

ただ、1度登録した後でもユーザーが自由に編集できる要素なので、必死に他アプリの調査をしてなんとしても重複を回避しなければいけないというほどのものではないです。提案フレーズ検討時に、自分の端末にインストール済みアプリたちのショートカットをざっと確認してみる程度で十分だと思います。

検証する端末
  • サポートしているOSバージョンのうち、一番低いメジャーバージョンの端末で確認
    • OSバージョンが進む毎に聞き取り性能が大きく向上している
  • サポートしているOSバージョンの範囲で極力スペックの低い端末で確認
    • ハード面でも聞き取り性能が異なる

StudyplusでQRコード画面の提案フレーズ検討時に、アプリの略称を提案フレーズに含めた際の検証をしました。 具体的には、Studyplus内にてユーザーの間で「スタプラ」と略して呼ばれがちなため、提案フレーズに「スタプラ」を含めようとしました。

しかしながら、最低サポートバージョンであるiOS 12、かつiOS 12まででサポートを終了した古いiPad端末で検証したところ、10回中8回聞き取り失敗したので諦めました。 こちらの検証をした筆者は比較的に機会に拾われやすい声質と自負しているので、採用してしまうと使われなくなる恐れがあると察知しました...

検証する環境
  • できるだけユーザーが実際に利用しうる環境と近い状態で試す
    • スポーツジムで利用するアプリなら実際に持っていき聞き取りを試すなど
  • 机の上に平置きした状態で試す
    • 角度や端末への接地面の都合からか、手で持つより意図しない雑音が拾われがち

アプリ略称の検証をした際に、机の上に平置きで10回中8回失敗、手に持った状態では10回中4回失敗でした。

高い確率で発生しうるユースケースとして、端末を机の上などに平置きした状態での検証まではしておくことをお勧めします。

参考

NSUserActivityオブジェクトのプロパティ⇔画面の表示箇所対応表

f:id:m_yamada1992:20201012201722j:plain

実装・発表にあたり参考にしたWebページのURL

さいごに

このLTに登壇したきっかけが「ひとまず特定画面を開く単純なSiri Shortcuts機能をリリースしよう!」と調査・実装・検証・ドキュメントまとめて社内展開まで行なったことです。それらの対応にかかったのが合計で3営業日。

Siri ShortcutsはiOS 12から13へアップデート時点で大幅に挙動が変わっている箇所がそこそこあります。 にも関わらず、Apple公式の一部ドキュメントすらiOS 12段階の情報で更新が止まっている箇所がありました。 その辺りの検証に大いに戸惑い、盛大につまづいた時間も込みで3営業日です。

それらの経験を踏まえて、「これくらい最初から情報がまとまっていれば1日は短縮できた」と思える情報を詰め込んで発表してみました。

ユーザーが頻繁に使う画面に心当たりがあれば是非、実装してみてください。実装の際にこちらの記事が少しでも助けになれば幸いです。