SiriKitを使ってSiri IntentやSiriショートカットの開発をしてみる

#はじめに
スマートスピーカーの開発というとAlexaやgoogle assistant, LINE Clovaの開発を思い浮かべがちですが、
最近SiriKitを使ったVUIの開発をしてみたくなり、勉強しながらやってみました。

それでできたのがSmartphone key for silviaという車のキーレスシステム。
開発したアプリでSiriKitを使い、音声認識の結果をBLE経由でハードウェアに命令を送るということをしてみました。

<iframe width="1280" height="720" src="https://www.youtube.com/embed/9BucSBQ-5Ck" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

Siriの開発をして気づいたことは、Siriは他のスマートスピーカーと違い、スマートフォンのUIの一つであるということ。
だから、スマートスピーカーでは敷居の高かったハードウェア連携が簡単にできます。
こんなSiriの世界を体験してもらいたいと思い、この記事を書くことにしました。

簡単ではありますが、お付き合いいただけると幸いです。

#SiriKitの開発は２種類ある
とりあえずSiriを使ってみたいと勉強を始めたところ、実はSiriの開発は大きく２種類ある事がわかりました。

・Siri Intents
・Siriショートカット

これらは同じようで実は少し違っており、用途によって使い分けるのがいいのではと思います。
動画を見てもらうとこの２種類の反応の違いを見る事ができます。

以下メリット、デメリットを表にしてみました。

|            | Siri Intents                                                                                                                       | Siriショートカット                                                                                                                                                                                                                                                             | 
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 
| メリット   | ・多くの発話に対応。適当に話しても処理してくれる<br>・認識の際に必要なスロット（車の名前等の固有名詞）を追加し、認識させる事が可能 | ・コマンド入力となるため、ユーザーが発話内容を設定し、使えるようになる（=開発者が対話モデルを考える必要はない）<br>・Homepodでも使える<br>・スマートフォンがスリープしていても使える<br>・音声だけではなく、ショートカットのコマンドとして使えるので、オートメーションもできる | 
| デメリット | ・スリープ時には使えない（アンロックしてと言われる）<br>・Siriドメインと呼ばれる特定のIntentでしか使えない                         | ・対話内容が固定される。（"〇〇を実行します")<br>・柔軟な発話に対応しようと思ったら、ユーザー自信が追加しなければならない                                                                                                                                                      |                                                                                                                  |               

これを見てお気づきの方はいらっしゃるかと思いますが、Siri Intentは所謂”スキル開発”で、Siriショートカットは”定型アクションの設定”という感じです。
開発者は自分のアプリをスキル対応させるか、またショートカット対応させるのか、それともその両方の対応をさせるのかを選ぶ事ができます。

ちなみにIntentは”Siriドメイン”と呼ばれており、[Appleの開発者ドキュメント](https://developer.apple.com/jp/documentation/sirikit/)にて確認が可能です。
このドキュメントを見て驚いたのは”Car command”というドメインが存在しており、車のドアのロックアンロックや、どこにあるかホーンやライトでお知らせ、またEVの場合はバッテリー残量を確認できるようになっていた事。ちなみに、これを使っている車は[TESLA](https://tesla-info.com/siri.php)くらい。Appleは自動車業界に参入しようとしているんだと垣間見た気がします。

今回Smartphone key for silviaでもこの機能を使ってみました。まぁマニアックなドメインなのでほとんどの開発者はこれを使うことはないでしょう笑


#実際に開発をしてみる
Siriの開発は有料の開発者でなければできないので、少し敷居が高いかもしれませんが、Siriの開発はやってみると結構簡単。
ただし躓くポイントもいくつかあるので、その辺りをメインに紹介したいと思います。

##SiriKitの使い方
SiriKitを使うにあたり、どういう処理が行われ、自分のアプリで使えるようになるか理解します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324560/033d7828-6864-bb63-455d-d2bfbc567dd5.png" width=600">
画像を見るとわかるように、Intents App Extensionというのを入れれば使えるということが書かれています。
ちょっとわかりにくいですが、要はSiriKitを自身のアプリに導入するのはとても簡単で、ケイパビリティでSiriを有効にすればOKです。
これでSiriが使えるようになります。
(とはいえ、もちろんinfo.plistでprivacy設定も追加しなければなりません。)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324560/cb706639-5ceb-077c-40f0-ea4915b1e53e.png)

も少し詳細に知りたい方はこちらをご覧ください。

https://developer.apple.com/jp/documentation/sirikit/

## Intentを呼び出す
まずはIntentを設定します。
このIntentの設定はTARGETSのGeneralにあるSupported Intentsに使いたいIntentを追加することで使えるようになります。

![スクリーンショット 2021-12-21 0.47.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324560/92e26b58-ff43-6146-e7ba-813e23146ad3.png)

これでコードを書く準備はできました。
ユーザーが発話をしたら自分が書いたコートが呼ばれるようになります。でもどうやって、呼ばれるのでしょうか？
Siriの場合、すでにSiriドメインで決まっていたりショートカットでユーザーが決めるため、自動的にどのIntentが呼ばれたのか判断してくれます。そのため、コードを見ると以下のように同じ functionからそれぞれのintentの処理を呼び出すようになっています。
例えば、
`CarLockActionIntent`はSiriショートカット用のIntent
`INActivateCarSignalIntent`はSiriドメインのIntent
になります。
このfunctionはAppDelegateに追加してください。`import Intents`または`import IntentsUI`をする事で呼び出しができるようになります。

```AppDelegate.swift

import Intents

    func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any? {
//        print(intent)
//        print("call here")
        switch intent {
        case is CarLockActionIntent:
            return CarLockHandler()
        case is CarHornActionIntent:
            return CarHornHandler()
        case is INActivateCarSignalIntent:
            return ActiveCarSignalHandler()
        case is INSetCarLockStatusIntent:
            return SetLockHandler()
        case is INGetCarLockStatusIntent: //アンロックされているかロックされているか知りたいはずなのに,successしかない・・・
            return GetLockHandler()
        default:
            return self//何もしない
        }
            
    }
```

Intentの判別がついたら `CarLockhandler()`等が呼び出され、その中に実際に実行したい処理を書いていきます。
ここでは1つの例として`GetLockHandler`を挙げてみます。このハンドラーは車のドアがロックされているか、それともアンロックされているかを確認し、教えてくれるというSiriドメインで定義された機能を実現しています。ほとんどBLEの処理を書いているだけなのですごくわかりにくいですが、SiriKitに関する処理はコメントを記載しましたので、コードをご覧ください

```IntentActions.swift

class GetLockHandler:NSObject,INGetCarLockStatusIntentHandling{//ここでINGetCarLockStatusIntentHandlingを追加。
//ちなみに、ショートカットの場合、TargetsのSupported Intentsの項目で自分のIntentを設定すると、自動的にクラス名が出来上がります。

    //このhandle functionは必須のfuncになる。これ以外にもユーザーが発話したことを
    func handle(intent: INGetCarLockStatusIntent, completion: @escaping (INGetCarLockStatusIntentResponse) -> Void) {
        print("call confirm get lock handler")
        print(intent)
        
        var responseToSiri:INGetCarLockStatusIntentResponse //ここでレスポンスの結果を格納する変数を定義。
        //

        //そもそもデバイスが接続されていなかったら実行できない
        if intent.carName == nil || manager.inConnect == false {
            print("doesn't control")
            completion(INGetCarLockStatusIntentResponse(code: .failure, userActivity: nil))
        }else{
            responseToSiri = .init(code: .success, userActivity:nil)
            if(manager.doorCloseState == true && manager.doorLockState == true){ //ドアが閉じていて、ロックされている
                responseToSiri.locked = true //Siriドメインなので、lockedなどのパラメータは決まっている
            }else{ //manager.doorCloseState == false || manager.doorLockState == false
                responseToSiri.locked = false
            }
            completion(responseToSiri)
//このcompletionを返す = 処理が完了となります。completionを返す前に同期処理を行い、全ての処理が終わってからcompletionを実行するようにする必要があります。
//completionを返すとSiriが定型のレスポンスを返します。
        }
    }
}
```
コードとしてはこれだけで終わり。どこにも自然言語処理をしている感じがでていませんが、実は変数（スロット）の確認もする事ができます
`func confirm`や `func resolve~~~~`という関数も自動的に生成されます。
・resolve → パラメータはスロットで求められているもであるか検証、判断する
・confirm → ユーザーにそのパラメーターでOKか確認
・handle  → 処理を実行
という処理を行う事ができます。これらどれも`completion`を使う事、そしてresolve → confirm → handleの順番で自動的に処理されるといったルールがあります。

先述のコードはSiriドメイン、つまりSiri Intentの処理を書く場合のコードでした。
次にSiriショートカットの場合の処理を見てみましょう

```IntentActions.swift
class CarHornHandler:NSObject,CarHornActionIntentHandling{

    
    func handle(intent: CarHornActionIntent, completion: @escaping (CarHornActionIntentResponse) -> Void) {
        
        var res:CarHornActionIntentResponseCode! //繋がってなかったら動かない
        
        if(manager.inConnect == false || manager.keySetState == true || manager.ignOnState == true){
            res = .failure
        }else{
            NotificationCenter.default.post(name: .orderHornAction, object: nil) //ホーンを動かすように命令
            res = .success
        }
        completion(CarHornActionIntentResponse.init(code: res, userActivity: nil))
        
    }
}
```
こちらみてもらうと分かると思いますが、実は構造は同じです。プログラミングで見ると実はほぼ同じなんですね。
ただし、Siriショートカットの場合、`SiriKit Intent Definition File`を作成して、自分でレスポンスの種類や変数の種類、またショートカットのページで表示する内容を定義する必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324560/72990700-f95b-5bff-0a76-69d465857663.png)

詳細は割愛しますが、こんな感じでショートカットの設定を行う必要があります。
![スクリーンショット 2021-12-21 1.06.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324560/e0bdae0f-a6da-5359-cda4-65381c1b56a1.png)

これでアプリをコンパイルすれば、ショートカットで機能が使えるようになっていたり、自分のアプリで音声コマンドを受付できるようになる事がわかります。

## 開発時に出逢った躓きポイント
実はSiriKit、ただアプリをスマホに書き込みしただけではうまく動かない事があります。
私は実際動かず、自分のコードがイケてないんではないかと、何度も確認をしました。
現象としては、発話してもIntentが呼ばれない事です。

この解決方法をネットで調べに調べ、行き着いた答えは**スマホの再起動**です。
スマホを再起動するとIntentが呼ばれるようになります。これはアプリ開発の中で初めての経験でした。再起動したら動くんだ・・・
もし同じような現象に出逢ったら、再起動をお試しください。


#さいごに
Alexaスキルの開発からSiriKitを使ったSiri開発を今回初めてやってみました。
正直Aleaxスキル開発の醍醐味である対話モデルの開発をすることはできないためあまり面白みはないかもしれません。
ただスキル開発とは違い、ハードウェア連携やWebAPI、またアプリ内の他の機能を実行するといった、ユーザーの命令に対するアクションのバリエーションが多いため、音声をコマンドに何かをやってみたい！という人にはおすすめの機能だなと思いました。

またSiriKitの記事を見ると古いものが多いですが、実は開発がしやすくなっているようです。
今回開発するにあたり、他の方が書いた記事よりも公式ドキュメントやWWDCのセッションを見ながら作ったため、少し違う内容になっているかと思います。
これでもちゃんと動いています。

SiriKitにご興味持っていただけたら幸いです。