【WidgetKit】ウィジェットの編集項目をカスタムする方法

フリューAdvent Calendar 2022の4日目を担当します、f_nksksです。
今回はSwiftUIのWidgetKitについて、ウィジェットの編集画面をカスタムする方法について記事にしていこうと思います。
よろしくお願いします。

# やりたいこと
 - ウィジェットを長押し→ウィジェットの編集ができる
 - ウィジェット編集画面で、任意の項目を選択できる
 - 項目選択した後、その項目に属する追加の設定項目が出現する
![画面収録_2022-12-04.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957449/63ebdcc1-8cad-b9a4-9fc7-9fa3e11dda04.gif)

※設定内容のウィジェット側への反映や、アプリ側で設定したデータの連携などは、また別途Qiitaの記事に書く予定なので、一旦ウィジェット編集画面が動くようになっていればOKとします。

# 前提
- プロジェクト作成済
- Widget Extentionをターゲットに追加済みで、ウィジェットが表示できる状態になっている

## 開発環境
- XCode 13.3.1
- Target: iOS 14.0

# 手順
## ウィジェットの編集画面を表示
ウィジェット編集画面を表示するために、まずはIntentHandlerを追加します。
新規ターゲットとして`Intents Extension`を作成します。
<img width=60% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957449/d8387c01-cd10-0bfe-45a7-4a48ffd16346.png" />
ターゲット直下のInfo.plistを開いて、`IntentSupported`のItemとして`ConfigurationIntent`を追加しておきます。
<img width=80% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957449/49492c1c-50c4-2828-00fe-0428d2e788e1.png" />

続いて、Widget Extentionターゲットにある、Configrationファイル（ターゲット名.intentdefinition）の中身を変更します。
ひとまずはParametersにある`＋`ボタンから1つパラメータを追加します。
色々設定できそうですが、ひとまずTypeを仮で`Integer`にしておきます。パラメータ名はwidgetTypeとしておきました。
<img width=80% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957449/717f5997-d707-8bab-0326-159c1ee3eef8.png" />
ビルドしてアプリのウィジェットを長押しすると、クイックアクションメニューからウィジェットの編集画面が開けるようになりました。
数値が入力できるようになっています。
<img width=40% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957449/2c726bb5-0ca7-1e71-baae-a2330f186f1c.png" />



## 任意の項目を設定できるようにする
これだけだと数値入力しか出来ないので、「タイプ1」「タイプ2」の文字列のどちらかを選択できるように修正していきます。

まずはWidget ExtentionターゲットのConfigrationファイルを編集します。
Target MembershipにIntents Extensionターゲットを追加しておきます。
<img width=50% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957449/c3445f02-0215-e2fa-baef-17e665a29419.png" />
先ほど作成したwidgetTypeパラメータについて、Typeから`Add Type`を選択します。
<img width=80% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957449/ae85a478-3dff-f07b-59fe-446433cf9a3c.png" />
するとTYPESに項目が追加されるので、名前をWidgetTypeに変更しておきます。（後々扱う型名になります）
<img width=40% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957449/a2de98ee-8d0c-72ff-d4f7-6d2c7efb0dd5.png" />
また、`Dynamic Options`の2つのチェックボックスにもチェックを入れておく必要があります。ここをtrueにしておかないと、動的に項目一覧を適用する事ができません。
<img width=80% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957449/2c9b063b-3f60-a9e9-e77d-8f74445b185e.png" />

続いて、Intents Extensionターゲット内の、`IntentHandler.swift`ファイルを開き、Target MembershipにWidget Extensionターゲットを追加します。
<img width=50% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957449/715707f4-9e0e-54d6-dc26-2eb857e0102a.png" />
コードにはIntentHandlerクラスが既に記述されているので、`ConfigurationIntentHandling`プロトコルを追加します。
```swift:
class IntentHandler: INExtension , ConfigurationIntentHandling {

    override func handler(for intent: INIntent) -> Any {
        return self
    }
    
}
```

エラーメッセージから`protocol stubを実装してください`と言われるので、fixするとWidgetType一覧を設定するためのメソッドを自動生成してくれます。
この中身を書き換えていきましょう。

WidgetTypeに`identifier`（固有のID）と`display`（選択時に表示される文字列）をそれぞれ定義し、一覧にしてcompletionの第一引数に渡してやります。
今回はタイプ1/タイプ2が選択できればいいので、以下のように記述しました。

```swift:
func provideWidgetTypeOptionsCollection(for intent: ConfigurationIntent, searchTerm: String?, with completion: @escaping (INObjectCollection<WidgetType>?, Error?) -> Void) {
        let widgetTypeIdentifiers: [WidgetType] = [
            WidgetType(identifier: "type1", display: "タイプ1"),
            WidgetType(identifier: "type2", display: "タイプ2"),
        ]
        let allWidgetTypeIdentifiers = INObjectCollection(items: widgetTypeIdentifiers)
        completion(allWidgetTypeIdentifiers, nil)
    }
```

この状態でウィジェット編集画面を開くと、先ほど設定したタイプ1/タイプ2をそれぞれ選択できるようになっているかと思います。
<img width=40% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957449/f9961627-ef3e-a795-df6b-321dcf2777cf.png" />

## 子パラメータを追加する
ウィジェットの編集画面でタイプ1を選択した時に、追加パラメータとして「タイプ1-1」を、
タイプ2を選択した時に「タイプ2-1」の項目を表示するように設定していきます。

widgetTypeを追加した時と同様に、ConfigrationのパラメータにchildType1_1を追加します。（一旦表示できればいいので、Typeは仮）
`Relationship：Parent Parameter`に対し、今回親パラメータとしたい`widgetType`を選択します。
<img width=80% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957449/54a58580-4b93-851a-07e7-698f7220dc20.png" />

さらに`Show If Parent`が設定できるようになったので、ここを`identifier contains`に変え、先ほどIntentHandlerの実装で設定したWidgetTypeのidentifier（今回は"type1"）を記入します。
<img width=80% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957449/5cce0185-98ed-34e6-bd50-fcb35c59e40f.png" />
これで、「WidgetTypeでタイプ1が選択されたときに、ウィジェット編集の選択項目にタイプ1-1が追加される」状態になりました。
タイプ2についても同様にパラメータを追加し、`Relationship`の`Value`を"type2"にすることで、同様の状態となります。
<img width=40% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957449/fa2d0f38-374f-031d-9ab1-0ef49bbbf3f3.png">

注意点なのですが、

widgetType（親） -> childType1_1（子） -> ×（孫）

といったように、childType1_1に対し、更に子パラメータを追加することはできないようです。
Relationshipは1階層しか出来ない前提でウィジェット編集画面の構成を考える必要があります。

# 最後に
無事、ウィジェットの編集ができるようになりました。
設定できるだけだと汎用性がないので、アプリ側で設定・保存した値を読み込んだり、ウィジェットに反映する必要があります。
今後そちらの方法についても記事にしていきたいと思います。