最新版！！！動的な設定が可能なWidgetを作成してみよう

:::note
これは[フェンリル デザインとテクノロジー Advent Calendar2024](https://adventar.org/calendars/10935) 20日目の記事です。
:::

# はじめに

👦「ウィジェットの実装、ハードルが高くないですか？」

🧒「アプリ本体部分とは概念が異なるため、抽象的に理解することすら難しい」

👨「iOSやXcodeのアップデートがあるたびに、異なる実装が提供されてしまい、どんどん種類が増えて正解がわからない」

そんなみなさんに向けて、「実はそんなことないんだよ」と優しく背中を押したいという気持ちを込めて書きました。

本記事では、理論よりも具体的な実装に焦点を当てることで、動かしながら理解してもらうことを目的としています。

具体的には、プロジェクトファイルの作成から動的な設定が可能なウィジェットの実装までを行います。


## 1. プロジェクトファイルを作成

まず、ウィジェット練習用のプロジェクトファイルを作成します。

特別な設定は必要ありません。

![スクリーンショット 2024-12-16 8.27.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935016/a196a2d0-8daa-d430-5c32-fe7c9e31b009.png)


## 2. Targetを追加

ウィジェットはアプリ本体とは別のプロセスで実行されるので、Targetを別で作成する必要があります。


`File` → `New` → `Target` → `iOS` → `Widget Extension`と進みます。

下記のようにウィジェット用Targetを作成しましょう。

![スクリーンショット 2024-12-16 8.29.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935016/6ac814eb-b478-344a-5471-68427df066f2.png)


`Widget Extension`を作成すると、下記のファイルが生成されます。

- `AppIntent`: アプリの特定の機能や操作を定義するためのファイルです。これにより、ウィジェットやショートカットから直接アプリの機能を呼び出すことが可能になります。
- `Widget`: ウィジェットの外観や動作を定義するファイルです。SwiftUIを使用して、ウィジェットのUIや表示するデータを構築します。
- `WidgetBundle`: 複数のウィジェットをまとめて提供するためのファイルです。これにより、ユーザーはアプリから複数のウィジェットを一度に利用できるようになります。

Simulator上で実行してみると、すでにウィジェットが追加されていることがわかります（簡単😃）。

![Simulator Screenshot - iPhone 16 - 2024-12-16 at 08.47.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935016/d343cfef-9a0b-1083-ece2-b5492491ab71.png)


:::note warn
**エラーが出た場合**

もし、ここで`“~Widget” failed to launch or exited …` エラーが出る場合は、証明書の設定を見直してみてください。

Targets → Signing & Capabilities で警告が出ているかを確認してみましょう。
:::

</aside>

## 3. ウィジェットの見た目を変更

ウィジェットの画面は`Widget`ファイル内にViewで定義されており、SwiftUIを用いたレイアウトが可能です。

試しに見た目を少し変更してみましょう。

まず、時刻表示を消してスッキリさせます。

```diff_swift
struct EmojiWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        VStack {
-           Text("Time:")
-           Text(entry.date, style: .time)
-					
            Text("Favorite Emoji:")
            Text(entry.configuration.favoriteEmoji)
        }
    }
}
```

次に、背景色を変更してみましょう。

Viewに付与されている`.containerBackground` モディファイアの第一引数を任意の色に変更すればOKです。

```diff_swift
struct EmojiWidget: Widget {
    let kind: String = "EmojiWidget"

    var body: some WidgetConfiguration {
        AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
            EmojiWidgetEntryView(entry: entry)
- 	            .containerBackground(.fill.tertiary, for: .widget)
+               .containerBackground(.orange.opacity(0.4), for: .widget)
        }
    }
}
```

| 変更前 | 変更後 |
| --- | --- |
| ![Simulator Screenshot - iPhone 16 - 2024-12-16 at 08.47.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935016/f412764f-2345-6d87-dddc-30221972db18.png) | ![Simulator Screenshot - iPhone 16 - 2024-12-16 at 11.15.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935016/b6edc4c2-f6de-971e-81aa-619fd6a24b0d.png)|

ちなみに、任意の`View`を割り当てることもできます。

下記の例では、絵文字の画像に白い半透明の`View`を重ねたものを背景としています。

```swift
struct EmojiWidget: Widget {
    let kind: String = "EmojiWidget"

    var body: some WidgetConfiguration {
        AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
            EmojiWidgetEntryView(entry: entry)
                .containerBackground(for: .widget) {
                    Image("emoji")
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                        .overlay {
                            Color.white.opacity(0.6)
                        }
                }
        }
    }
}
```

![Simulator Screenshot - iPhone 16 - 2024-12-16 at 11.24.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935016/f3087be2-aa6b-1928-2b7b-81a3b4720cb6.png)


## 4. ウィジェットの動的な設定を追加

iOS16から、ウィジェットの設定をユーザーが変更できるようになりました。みなさんは使ったことはありますか？（私はありませんでした）

実は、手順2を終えた時点で既にその機能を作成できています。

ホーム画面の「ウィジェットを編集」から、表示する絵文字を変更してみましょう。

![captcha1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935016/1df0ded0-9af9-a91d-faf6-6e916a8acad0.gif)

ウィジェットの要素を追加するには、`AppIntent`構造体内に`@Parameter`をつけた変数を追加すればOKです。

試しに、ウィジェットの背景色をStringで定義してみましょう。

`AppIntent`で定義したプロパティは、`entry.configuration.{プロパティ}` の形式で取得することができます（詳細は割愛）。

```swift
struct ConfigurationAppIntent: WidgetConfigurationIntent {
    static var title: LocalizedStringResource { "Configuration" }
    static var description: IntentDescription { "This is an example widget." }

    // An example configurable parameter.
    @Parameter(title: "Favorite Emoji", default: "😃")
    var favoriteEmoji: String
    
    // 追加
    @Parameter(title: "Background Color", default: "White")
	var backgroundColor: String
}
```

```swift
struct EmojiWidget: Widget {
    let kind: String = "EmojiWidget"

    var body: some WidgetConfiguration {
        AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
            EmojiWidgetEntryView(entry: entry)
                .containerBackground(backgroundColor(text: entry.configuration.backgroundColor), for: .widget)
        }
    }
    
    private func backgroundColor(text: String) -> Color {
        switch text {
        case "White":
            return .white
        case "Orange":
            return .orange.opacity(0.4)
        default:
            return .clear
        }
    }
}
```

実行すると、`Orange`と入力すると背景がオレンジ色になり、`White`と入力すると白色に戻ることが確認できます。

![captcha2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935016/64ac33a7-8a25-33f0-4818-1c4c50f93e26.gif)

## 5. Picker表示の追加

ここまではウィジェットの設定を文字列で変更していましたが、実は他にも様々な形式が用意されています。

最後に使用頻度の高いPicker表示について解説します。

Picker表示を行うには、`@Parameter`を持つプロパティを`AppEnum`プロトコルに準拠させた`Enum`形式で定義する必要があります。

`AppEnum`とは、`Enum`形式で定義されたアプリの機能を`AppIntent`を通してユーザーに提供する際に定義されるプロトコルです。

つまり、こういうことだと解釈しています。

- **単なるEnumだと、情報が足りない**
    
    Picker表示する際のタイトルやアイコン画像などの情報がないため、プロトコルによって追加したい
    
- **AppIntent側で情報を統一したい**
    
    AppEnumはウィジェット以外にもSiriやショートカットで使用されるため、プロトコルによって統一した情報が欲しい
    

詳しくは公式の説明をご覧ください。

https://developer.apple.com/documentation/appintents/appenum

それでは、ウィジェットの背景色をPickerで変更する処理を実装してみましょう。

まず、背景色のデータを持つ`Enum`を作成します。

```swift
extension ConfigurationAppIntent {
    enum BackgroundColorPicker {
        case white, orange
        
        var color: Color {
            switch self {
            case .white:
                return .white
            case .orange:
                return .orange.opacity(0.4)
            }
        }
    }
}
```

その後、`AppEnum`に準拠させます。

```diff_swift
extension ConfigurationAppIntent {
    enum BackgroundColorPicker: String, AppEnum {
        case white, orange
        
+       static var typeDisplayRepresentation: TypeDisplayRepresentation {
+            TypeDisplayRepresentation(name: "背景色", numericFormat: "color")
+       }
+       
+       
+       static var caseDisplayRepresentations: [ConfigurationAppIntent.BackgroundColorPicker : DisplayRepresentation] {
+          [
+              .white: DisplayRepresentation(title: "white", subtitle: "白になります", image: .init(systemName: "snowflake")),
+               .orange: DisplayRepresentation(title: "orange", subtitle: "オレンジになります", image: .init(systemName: "carrot"))
+           ]
+       }
+         
        var color: Color {
            switch self {
            case .white:
                return .white
            case .orange:
                return .orange.opacity(0.4)
            }
        }
    }
}
```

`AppEnum`に準拠させるには、下記2つのプロパティを定義する必要があります。

- `typeDisplayRepresentation`: 列挙型全体の名前を指定します。ユーザーがショートカットアプリでパラメータを選択する際に、この名前が表示されます。
- `caseDisplayRepresentations`: 各列挙型のケースに対応する表示名を指定する辞書型配列を返します。ユーザーが特定の値を選択する際に、これらの表示名が使用されます。

その後、以前の手順と同様にプロパティを書き換えます。

```swift
struct ConfigurationAppIntent: WidgetConfigurationIntent {
    static var title: LocalizedStringResource { "Configuration" }
    static var description: IntentDescription { "This is an example widget." }

    // An example configurable parameter.
    @Parameter(title: "Favorite Emoji", default: "😃")
    var favoriteEmoji: String
    
    @Parameter(title: "Background Color", default: .white) // デフォルト値を変更
    var backgroundColor: BackgroundColorPicker // 型を変更
}
```

```swift
struct EmojiWidget: Widget {
    let kind: String = "EmojiWidget"

    var body: some WidgetConfiguration {
        AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
            EmojiWidgetEntryView(entry: entry)
                .containerBackground(entry.configuration.backgroundColor.color, for: .widget) // 変更
        }
    }
    
    // backgroundColorメソッドを削除
}
```

これらを変更した後に実行し、ウィジェットの編集を行う際にPicker表示が行われたら成功です👏

|ウィジェットの編集|Picker表示|
|-|-|
|![captcha3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935016/d864c31b-74f3-c3dc-ae33-3797562df02c.gif)|![Simulator Screenshot - iPhone 16 - 2024-12-16 at 13.46.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935016/e79b7d90-75ee-bfd9-9a48-2d45522b5dd1.png)|

# おわりに

以上が最新版の実装についてでした（2024年12月時点）。今までいろいろな種類の実装を探してきた方も、まずは本記事を基に試してみてはいかがでしょうか。

ここまで画像やコードをたくさん挿入したため、手順が多く見えるかもしれませんが、内容はとても簡単です。慣れてしまえば10分程度で実装できてしまうと思います。
わかりやすいとのお声があれば、次回以降はインタラクティブなウィジェット（ウィジェット自体をタップしてアクションが実行されるもの）の実装について記事を書きたいと思います。

※ サムネにしたいと思って作成したが、記事の内容と異なるため泣く泣く下に追いやられたホーム画面

![スクショ.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935016/914a4b69-472b-0438-4a77-b2d4de3c378a.jpeg)