[SiriKit]Siriから料金の支払いをする

「Hey Siri、イーカリット（←アプリ名）で学費1000円を支払って」

iOS 10以降、SiriKitを用いてSiriからアプリの機能を呼び出すことができるようになりました。

iOS 10.3からは、料金の支払機能の呼び出しに対応しています。
これは、iOSが特別な決済機能を提供するというものではありません。
詳しくは後述しますが、SiriKitはあくまでアプリの機能に対するインターフェースになります。

本稿ではSiriによる料金の支払いを実装しますが、
他の機能を呼び出す場合も参考になるよう、できるだけ一般的に説明します。

![IMG_1528.PNG](https://qiita-image-store.s3.amazonaws.com/0/50559/a84d56d1-db3a-c881-d994-31f8e9832ceb.png "IMG_1528.PNG")

## SiriKitとは
SiriKitという言葉は、UIKitのようにそれ自体がフレームワークの名前ではありません。
SiriKitは、IntentsフレームワークおよびIntents UIフレームワークから構成されます。

Intentsフレームワークは、ユーザーがSiriに要求した内容、つまりユーザーの意思（Intent）に対して処理を行うための機能を提供します。
（※Siriだけではなく、Mapsとの連携にも用いられます）

Intents UIフレームワークは、Siriの応答で独自のUIを表示したい場合、任意で使用することができます。

SiriKitでは、アプリのどのような機能でも呼び出せるわけではありません。
アプリの機能の領域をドメインといいますが、以下に挙げるドメインのみがサポートされます（iOS 10.3時点）。

+ VoIP通話
+ メッセージ交換
+ 決済
+ 写真
+ ワークアウト
+ 配車予約
+ CarPlay（自動車製造会社のみ）
+ レストラン予約（Appleからの追加サポートが必要）


「アプリの機能を呼び出す」という言い方をしましたが、実際には **App Extension** を実装することになります。

#### App Extension
App Extensionとはアプリ間で連携するための仕組みです。コンテンツの共有や写真の加工、Todayウィジェットの作成など、目的に応じた様々なタイプのExtensionが用意されています。

その名の通りアプリの拡張であり、アプリ本体とは別の実行ファイルとしてビルドされます。

アプリ本体とセットで作成し、App Storeへリリースする際はアプリ本体にバンドルされるかたちになるので、さほど煩雑ではありません。

ただし、あくまでアプリ本体とは別のターゲットになるので以下の点を検討してください。

+ アプリ本体とコードを共有するために **Embedding Frameworks** を活用する
+ アプリ本体とCoreDataやUserDefaultsのデータを共有するために **App Groups** を構成する

本稿では、App ExtensionおよびEmbedding FrameworksやApp Groupsの詳しい説明は行いません。Appleのドキュメントなどを参照してください。


## プロジェクトの設定
ここからはXcodeを開き実装していきます。
まず、新しく作成したプロジェクト（または既存のプロジェクト）に対し設定を行います。

### Display Nameの設定
アプリ本体のDisplay Nameに設定した名前でSiriはアプリを認識します。
今回、アルファベット表記だとうまくアプリ名を認識してくれなかったのでカタカナの名前にしました。

![170423-0002.png](https://qiita-image-store.s3.amazonaws.com/0/50559/30ac3cc3-948d-49df-2291-516c19ad5d59.png "170423-0002.png")


### Capabilitiesの設定
プロジェクト設定のCapabilitiesでSiriをONにします。
![170423-0003.png](https://qiita-image-store.s3.amazonaws.com/0/50559/752d2163-4249-77df-a699-255e00e6b44f.png "170423-0003.png")


### ターゲットの追加
以下の手順で、Intents Extensionのターゲットを追加します。
Siriの応答を独自のUIにする場合は、同様の手順でIntent UI Extensionも追加します。

0. プロジェクト設定のTARGETS欄の下部にある＋ボタンを押します。
0. ターゲット選択ダイアログから[Intents Extension]を選択して[Next]を押します。
0. Intent Extensionの名前などを入力します。
    [Include UI Extension]にチェックをしておくと、Intent UI Extensionも同時に作成できます。
0. Extension作成後、追加したExtensionのスキームを有効にするかどうか聞かれた場合は、[Activate]（有効化）を選択するとよいでしょう。
 
![170423-0005.png](https://qiita-image-store.s3.amazonaws.com/0/50559/eb530f56-ea06-f589-714e-2ddfca9f0df4.png "170423-0005.png")

![170423-0006.png](https://qiita-image-store.s3.amazonaws.com/0/50559/0e74a3ca-98a1-46d5-415d-a2012b48bc2d.png "170423-0006.png")

![170423-0007.png](https://qiita-image-store.s3.amazonaws.com/0/50559/be779d5e-af94-e9ce-8090-190d177adbb1.png "170423-0007.png")

### 使用するIntentをInfo.plistに指定
Intentは、「料金の支払いをして」というようなSiriへの要求です。Swiftではクラスとして表現されます。

要求の種類ごとにIntentクラスのサブクラスが定義されており、料金の支払いは`INPayBillIntent`として定義されています。
（他の要求に対応するIntentについては、脚注のドキュメント[^1]を参照してください）

**Intents Extensionの**Info.plistを開き、 
[NSExtension]>[NSExtensionSttributes]>[IntentsSupported]配下のItemに、使用するIntentを指定します。
デフォルトでメッセージ関連のIntentが3つ指定されていますが、必要なIntent（今回はINPayBillIntent）のみ指定して、不要なキーは削除しましょう。

Intents UI Extensionを使用する場合は、そちらのInfo.plistにも同様の設定をしてください。

![170423-0010.png](https://qiita-image-store.s3.amazonaws.com/0/50559/ff004ef8-c3df-12ee-3e0b-6ae40683412b.png "170423-0010.png")

### デバッグの方法
ターゲットごとにスキームは異なります。
ステップ実行などのデバッグをしたい場合は、アプリ本体・Intent Extension・Intent UI Extensionのうち、対象のものを選択して実行してください。

Extensionのスキームで実行しようとすると、以下のようなダイアログが表示されるので、Siriを選択してください。

![170425-0004.png](https://qiita-image-store.s3.amazonaws.com/0/50559/ae6a259a-9c17-1888-871c-20ebaf638787.png "170425-0004.png")


## Siriとアプリを連携する許可を求める
Siriとアプリを連携するためにはユーザーの許可が必要です。

### 許可を求めるアラートに表示する文言を設定する
アプリ本体のInfo.plistに`NSSiriUsageDescription`キーを追加して、アラートに表示する文言を指定します。

下図のようにInfo.plistをプロパティリスト形式で開いた場合は、NSSiriUsageDescriptionキーは`Privacy - Siri Usage Description`と表示されます（自動で表示が切り替わります）。

![170423-0011.png](https://qiita-image-store.s3.amazonaws.com/0/50559/5466a312-334a-d4b3-5297-c60178e0d489.png "170423-0011.png")

### Siri連携の許可を求めるアラートを表示する
`INPreferences.requestSiriAuthorization`メソッドで、許可を求めるアラートを表示します。

```AppDelegate.swift
import Intents

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    // 〜 略 〜
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        INPreferences.requestSiriAuthorization { status in
            if case .authorized = status {
                print("authorized")
            } else {
                print("not authorized")
            }
        }

            return true
    }
    // 〜 略 〜
}
```

## Siriへの要求を処理する
SiriKitを利用した実装の中心となる部分です。

### 使用する主なAPI
Siriとアプリの連携では、主に以下の役割を担うオブジェクトを使用します。

| 種類     | 説明                             | 料金支払いの場合        |
|----------|----------------------------------|-------------------------|
| Intent   | ユーザーの要求を保持する         | INPayBillIntent         |
| Handler  | ユーザーの要求に対して処理を行う | INPayBillIntentHandling |
| Response | Handlerが返す応答                | INPayBillIntentResponse |

これらのオブジェクトは要求の種類ごとにそれぞれ異なる型が定義されています。
詳しくは脚注のドキュメント[^1]を参照してください。

他にも、SiriKitにおける一連の処理の起点となる`INExtension`のサブクラスを実装したり、パラメータの検証結果を`INIntentResolutionResult`のサブクラスで表現したりします。

### おおまかな処理の流れ
Intent Extensionで行う処理のおおまかな流れは次のようになります。

0. Siriがユーザーの要求を受け付け、Intentを生成する
0. Intent ExtensionでIntentを受けとり、Intentの種類に応じたHandlerに処理を移譲する
0. ユーザーの要求に含まれるパラメータを検証・解決する（Resolve）
0. 処理が可能かどうか最終確認をする（Confirm）
0. ユーザーの要求を実際に処理する（Handle）

パラメータとは、たとえば「1000円の学費を支払い」という要求をした場合の「1000円」という決済額や「学費」という料金の種類を指します。

ResolveとConfirmの実装は必須ではありませんが、できるだけ実装がすることが推奨されています。

### SiriからIntentを受けとり、Intentの種類に応じたHandlerに処理を移譲する
INExtensionを継承したクラスにおける`handler:for:`メソッドが処理の起点になります。

Siriから受け取ったIntentの種類に応じて、実際に要求に対処するHandlerを返します。Handlerは、要求の種類に応じたIntentHandlingプロトコルを実装したクラスです。
（※IntentHandlingという親プロトコルがあるわけではありませんが、本稿では便宜的にIntentHandlingプロトコルと呼びます）

Xcodeが生成するテンプレートでは、INExtensionのサブクラスおよびHandlerはともにIntentHandler.swiftに同じクラスとして定義されています。

```IntentHandler.swift
class IntentHandler: INExtension, INPayBillIntentHandling {
    
    override func handler(for intent: INIntent) -> Any {
        // if intent is INPayBillIntent {
        //     // インテントの種類に応じて適切なHandlerを返す
        //     return MyPayBillIntentHandler()
        // }
        
        // 今回は自クラスで処理する
        return self
    }
    
    // 〜
```


### パラメータを検証・解決する（Resolve）
IntentHandlingプロトコルではパラメータごとにresolveから始まる名前のメソッドが定義されています。

これらのメソッドのなかでパラメータが妥当であるかを検証します。
ときにはパラメータをより適切なかたちに置き換えることもあるでしょう。

これらのメソッドの実装は任意ですが、できるだけすべてのメソッドを実装することが推奨されています。

当該メソッドの最後には`completion`を実行する必要があります。
検証結果は`INIntentResolutionResult`のサブクラスで表現し、completionの引数として渡します。

検証結果として[success（成功）]や[unsupported（サポートされていない）]、[disambiguation（曖昧さの解消）]など、さまざまなものを指定することができます。

当該パラメータがそもそも不要の場合は、[notRequired]を指定すればよいでしょう。

また、成功を表すsuccessの引数には解決済みとするパラメータを渡します。

これらの検証結果に応じて、Siriはパラメータが無効である旨を通知したり、再度聞き直したりします。

以下は決済額に対する検証の例です。

```IntentHandler.swift
    func resolveTransactionAmount(forPayBill intent: INPayBillIntent, with completion: @escaping (INPaymentAmountResolutionResult) -> Void) {
        // パラメータを検証する

        // パラメータはintentのプロパティに格納されている
        if let transactionAmount = intent.transactionAmount {
            if let currencyCode = transactionAmount.amount?.currencyCode, currencyCode == "JPY" {
                // 日本円ならパラメータとして受け入れる
                
                // successの引数には解決済みとするパラメータを渡す
                completion(INPaymentAmountResolutionResult.success(with: transactionAmount))
            } else {
                // 日本円以外はサポートしない（「その金額は扱えません」とSiriが応答する）
                completion(INPaymentAmountResolutionResult.unsupported())
            }
        } else {
            completion(INPaymentAmountResolutionResult.needsValue())
        }
    }
```

### 処理が可能かどうか最終確認をする（Confirm）
`confirm`メソッドで要求されている処理が実行可能かどうか最終確認します。たとえばWebサービスが利用可能かどうかなど、パラメータ以外の確認もここで行います。

confirmメソッドでも最後にcompletionを呼び出す必要があります。
ここでのcompletionの引数はResponse（INIntentResponseのサブクラス）になります。

ResponseにはSiriの応答に必要な情報をセットする必要があります。
料金の支払いでは、決済額をセットしなければSiriの応答に正しい決済額が表示されません。

ここでセットしたResponseは後述するIntents UIのなかでも使用することができます。

```IntentHandler.swift
    func confirm(payBill intent: INPayBillIntent, completion: @escaping (INPayBillIntentResponse) -> Swift.Void) {
        
        // 〜 略（外部サービスも含めて決済の準備が整っているか確認する 〜
        
        // Responseを生成
        let response = INPayBillIntentResponse(code: .ready, userActivity: nil)
        // 必要な情報をセット
        response.transactionAmount = intent.transactionAmount
        response.fromAccount = intent.fromAccount
        response.transactionScheduledDate = intent.transactionScheduledDate
        response.transactionNote = intent.transactionNote
        
        completion(response)
    }
```

### ユーザーの要求を実際に処理する（Handle）
`handle`メソッドでユーザーの要求を実際に処理します。
アプリ固有の決済処理はここで行ってください。

confirmと同じように、処理の最後にはResponseを引数としてcompletionを実行します。

Responseには成否を表すコードを設定します。


```IntentHandler.swift
    func handle(payBill intent: INPayBillIntent, completion: @escaping (INPayBillIntentResponse) -> Void) {
        
        // 〜 略（実際の決済処理） 〜
        
        let response = INPayBillIntentResponse(code: .success, userActivity: nil)
        completion(response)
    }
```

Intents UIを使用しない場合は、これで実装完了です。

## Siriの応答に独自のUIを表示する
Siriの応答に独自のUIを表示する場合は、Intents UIフレームワークを使用します。料金の支払いでは確認画面として表示されました。

Intents Extensionとは別のターゲットなります。
Intents UI Extensionをターゲットに追加していない場合は、すでに説明した[「ターゲットの追加」](http://qiita.com/shindooo/items/37af96822846f820c45b#%E3%82%BF%E3%83%BC%E3%82%B2%E3%83%83%E3%83%88%E3%81%AE%E8%BF%BD%E5%8A%A0)を参考に追加してください。

追加したIntents UIのディレクトリにはstoryboardが作成されています。これを編集することでビューのレイアウトを行います。

![170425-0002.png](https://qiita-image-store.s3.amazonaws.com/0/50559/650fb653-760f-72be-bd68-24a49c78a6fb.png "170425-0002.png")


対応するビューコントローラがロードされると、`configureWithInteraction:context:completion:`メソッドが呼ばれます。コードによるビューの編集が必要な場合はここで行います。

引数のinteractionにはResponseが含まれるので、解決済みのパラメータの値を利用することができます。

なおIntents UIでは、 **タップなどのユーザー操作を受け付けることはできません。**

```IntentViewController.swift
    func configure(with interaction: INInteraction!, context: INUIHostedViewContext, completion: ((CGSize) -> Void)!) {

        if let response = interaction.intentResponse as? INPayBillIntentResponse,
            let amount = response.transactionAmount?.amount?.amount?.intValue {
            // Responseを利用することができる
            courceNameLabel.text = "\(amount)円の\nPHP講座"
        }
        
        if let completion = completion {
            completion(self.desiredSize)
        }
    }

```

![IMG_1528.PNG](https://qiita-image-store.s3.amazonaws.com/0/50559/a84d56d1-db3a-c881-d994-31f8e9832ceb.png "IMG_1528.PNG")

## 所感
私は普段からSiriをよく利用します。
特にアラームやリマインダーのセットをするときは、手間が省けて大変便利です。

自分たちが開発するアプリも、適切にSiriに対応させることで利便性が向上すると思います。

ただ正直なところ、Siriによる発話の解釈、特にパラメータの解釈には不満もあります。今回、語順を入れ替えたり、別の同義語で話しかけたりするとうまく解釈してくれないこともありました。

今後、もしもサポートするドメインの拡張や精度の向上が実現されればより便利になるでしょう。期待しています。

## 作成したサンプル
https://github.com/shindooo/EKarritForSiriKitBillPayments

## 脚注
[^1]: [Intents Domains](https://developer.apple.com/library/content/documentation/Intents/Conceptual/SiriIntegrationGuide/SiriDomains.html)