プロジェクトにWidgetExtensionを追加して'WidgetFamily'ごとに表示してみる

## はじめに
ウィジェットの技術検証とキャッチアップを兼ねて社内で勉強会を開催しました。
今回は、WidgetExtensionを追加してWidgetFamilyごとに表示するところまでを記事にします。

## 環境
Swift 5.5
Xcode 13.1

## サンプルプロジェクト
GitHubにPushしました。気になる方はご覧ください。
https://github.com/ken-sasaki-222/WidgetTraining
![QR_961482.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643372/fb53454c-1f96-c7eb-4dbc-a98dc8cd61b9.png)

## ウィジェットをローカルでビルドする
#### ウィジェット機能を利用したいアプリの作成
`Xcode起動 → Create a new Xcode project → iOS → App`を選択して諸々設定
今回アプリ名は`WidgetTraining`とします。

#### Widget Extensionの追加
`File → New → Target → Widget Extension`を選択
今回は`Widget`という名前で作成します。

#### Include Configuration Intentとは
Widget Extensionを追加するタイミングで`Include Configuration Intent`という項目が表示されます。Configuration Intenを有効にすることで、ウィジェットで表示する内容をユーザーが選択できるようになります。
有効にしていない場合は、ユーザーが表示内容を選択できないタイプのウィジェットになります。

選択できる状態を`IntentConfiguration`、選択できない状態を`StaticConfiguration`と言います。

この内容は後ほどコードでも設定できるのでここでは概要を掴む程度で問題ありません。Finishを押してファイルを作成しましょう。

#### ウィジェットをビルドする
ファイルを作成したら`Widget.swift`を確認しましょう。

```Widget.swift
import WidgetKit
import SwiftUI
import Intents

struct Provider: IntentTimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), configuration: ConfigurationIntent())
    }

    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), configuration: configuration)
        completion(entry)
    }

    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []

        // Generate a timeline consisting of five entries an hour apart, starting from the current date.
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate, configuration: configuration)
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationIntent
}

struct WidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        Text(entry.date, style: .time)
    }
}

@main
struct Widget: Widget {
    let kind: String = "Widget"

    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            WidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
}

struct Widget_Previews: PreviewProvider {
    static var previews: some View {
        WidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}
```
このタイミングで`'Widget' is annotated with @main and must provide a main static function of type () -> Void or () throws -> Void.`とエラーが発生した場合は、構造体Widgetを以下のように編集します。

```Widget.swift
/// 省略///

@main
struct Widget: SwiftUI.Widget { // ここを編集する
    let kind: String = "Widget"

    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            WidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
}

///省略///
```

これでウィジェットをローカルでビルドすることができます。
続いて`WidgetFamily`を追加します。

## Widget Familyの追加
`systemSmall`、`systemMedium`、`systemLarge`、3種類のWidgetFamilyを追加します。

```Widget.swift
///省略///

@main
struct Widget: SwiftUI.Widget {
    let kind: String = "Widget"

    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            WidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) //ここを追加
    }
}

///省略///
```

このタイミングでウィジェットに表示されている時間（`Date()`）をテキストに変更してみようと思います。
`WidgetEntryView`に`EnvironmentObject`を追加して、WidgetFamilyごとにテキストを返します。

```Widget.swift
///省略///

struct WidgetEntryView : View {
    @Environment(\.widgetFamily) var family: WidgetFamily

    var body: some View {
        switch family {
        case .systemSmall:
            return Text("small widget.")
        case .systemMedium:
            return Text("medium widget.")
        case .systemLarge:
            return Text("large widget.")
        @unknown default:
            return Text("default.")
        }
    }
}

///省略///
```
構造体Widgetも以下のように書き換えます。

```Widget.swift
///省略///

@main
struct Widget: SwiftUI.Widget {
    let kind: String = "Widget"

    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { _ in
            WidgetEntryView()
        }
        .configurationDisplayName("widget training.")
        .description("select widget")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

///省略///
```

続いてプレビューでも各WidgetFamilyが表示されるように`PreviewProvider`を編集します。

```Widget.swift
///省略///

struct Widget_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            WidgetEntryView()
                .previewContext(WidgetPreviewContext(family: .systemSmall))
            WidgetEntryView()
                .previewContext(WidgetPreviewContext(family: .systemMedium))
            WidgetEntryView()
                .previewContext(WidgetPreviewContext(family: .systemLarge))
            WidgetEntryView()
                .previewContext(WidgetPreviewContext(family: .systemExtraLarge))
        }
    }
}
```

## 動作確認
iPhoneホーム画面で
![1種.002.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643372/b0b653b7-ba9d-8eba-22b6-2cee9514f58f.jpeg)
アプリ名を入力してウィジェットを検索
![3種.001.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643372/95a4df26-2973-1fe6-96a7-f3926acc7cdb.jpeg)
このように、`systemSmall`,`systemMedium`,`systemLarge`が表示されれば成功です。

## おわりに
今回はWidgetExtensionを追加してWidgetFamilyごとに表示する方法を書きました。

ウィジェットはあまりキャッチアップできてなかったので良い機会になりました。
引き続きサンプルプロジェクトを肥やしていこうと思います。

ご覧いただきありがとうございました。
ここはこうしたほうがいい、ここはちょっと違うなどありましたらご教示いただければ幸いです