WidgetKitで天気予報アプリ作ってみた〜Widget Extension追加編〜

## 投稿の経緯
前回投稿した**WidgetKitで天気予報アプリ作ってみた~位置情報取得&保存編~**の続編です。
今回は「Widget Extension」を追加して「WidgetBundle」を設定するところまでを書こうと思います。

前回の記事を見てない方は先に↓こちら↓を確認してください。

https://qiita.com/nkekisasa222/items/249240ad36128de05557
## 開発環境
Swift 5.5
Xcode 13.2.1

## サンプルプロジェクト
GitHubにPushしています。気になる方はご覧ください。
https://github.com/ken-sasaki-222/WeatherWidget
![QR_615642.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643372/8c43013f-9757-f7dd-5946-2684f30f63b7.png)

## Widget Extensionの追加
File → New → Target → Widget Extensionを選択。今回は`Widget`という名前で作成します。
#### Include Configuration Intentとは
![スクリーンショット 2022-02-02 22.49.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643372/9a9981d7-7ad7-8554-8bf8-8af76d333d26.png)
Widget Extensionを追加するタイミングで「Include Configuration Intent」という項目が表示されます。こちらを有効にすることで、ウィジェットで表示する内容をユーザーが選択できるようになります。
有効にしていない場合は、ユーザーが表示内容を選択できないタイプのウィジェットになります。

選択できる状態を「IntentConfiguration」、選択できない状態を「StaticConfiguration」と言います。
今回は有効にしなくて大丈夫です。そのまま「Widget Extension」を追加しましょう。

## Widgetファイルを分割する
```Widget.swift
import WidgetKit
import SwiftUI

struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date())
    }

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

    func getTimeline(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)
            entries.append(entry)
        }

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

struct SimpleEntry: TimelineEntry {
    let date: Date
}

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 {
        StaticConfiguration(kind: kind, 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()))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}
```
「Widget Extension」を追加するとこのように`Wifget.swift`ファイルが作成されます。

これは好みの問題ですが、このファイルにまとめてEntryやViewを書いてしまうと見通しが悪くなるのでいくつかのファイルに分けたいと思います。

それでは順番に対応しましょう。

#### TimelineProvider
```MediumWidgetProvider.swift
import WidgetKit
import SwiftUI

struct MediumWidgetProvider: TimelineProvider {
    func placeholder(in context: Context) -> MediumWidgetEntryModel {
        MediumWidgetEntryModel(date: Date())
    }

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

    func getTimeline(in context: Context, completion: @escaping (Timeline<MediumWidgetEntryModel>) -> ()) {
        var entries: [MediumWidgetEntryModel] = []

        // 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 = MediumWidgetEntryModel(date: entryDate)
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}
```
`MediumWidgetProvider`ファイルを作成してコードを移行します。
ミディアムサイズのウィジェット専用の「TimelineProvider」です。このstructはウィジェットで扱うデータを定義して、タイムラインにエントリーします。

#### TimelineEntry
```MediumWidgetEntryModel.swift
import WidgetKit
import SwiftUI

struct MediumWidgetEntryModel: TimelineEntry {
    let date: Date
}
```
`MediumWidgetEntryModel`ファイルを作成してコードを移行します。
ミディアムサイズのウィジェットのタイムラインに渡す「TimelineEntry型」のモデルとして扱います。

#### View
```MediumWidgetView.swift
import WidgetKit
import SwiftUI

struct MediumWidgetView: View {
    @Environment(\.colorScheme) var colorScheme
    
    var body: some View {
        Text("Hello, World!")
    }
}

struct MediumWidgetView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            MediumWidgetView()
                .previewContext(WidgetPreviewContext(family: .systemMedium))
                .environment(\.colorScheme, .light)
            MediumWidgetView()
                .previewContext(WidgetPreviewContext(family: .systemMedium))
                .environment(\.colorScheme, .dark)
        }
    }
}
```
`WidgetEntryView`を削除して新しく`MediumWidgetView`を作成します。ウィジェットのViewを管理します。
このタイミングで`@Environment(\.colorScheme) var colorScheme`を定義して、プレビュー画面でダークモードとライトモード両方のViewを確認できるようにしておきましょう。後々便利です。

#### Widget
```MediumWidgetMaster.swift
import WidgetKit
import SwiftUI

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

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: MediumWidgetProvider()) { entry in
            MediumWidgetView()
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
        .supportedFamilies([.systemMedium])
    }
}
```
新しく`MediumWidgetMaster`ファイルを作成してコードを移行します。このタイミングで`.supportedFamilies`でミディアムサイズを指定しておきましょう。

## WidgetBundleの設定
https://zenn.dev/usk2000/articles/5d36411b37bf43789979

コチラの記事と

https://developer.apple.com/documentation/swiftui/widgetbundle

公式ドキュメントがわかりやすいです。

「WidgetBundle」の中に複数のウィジェットを配置することで、異なるレイアウトのウィジェットが作りやすくなります。
現状はミディアムサイズのウィジェットのみの開発ですが、いつでも別サイズを追加できるように「WidgetBundle」を設定しておきましょう。

```WidgetBundle.swift
import WidgetKit
import SwiftUI

@main
struct WidgetBundle: SwiftUI.WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        MediumWidgetMaster()
    }
}
```
これで設定完了です。WidgetBundleに`@main`を付与して、WidgetMasterの`@main`は外しておきましょう。

現在のファイル構成はこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643372/ae5ba4c2-c721-db63-9b05-615016b3aae7.png)
ウィジェットとかWatchってTimeLineがあって構成が特殊だから、だいたいいつもこんな感じにまとめてます。
おすすめのまとめ方とかあれば教えてください！


## ウィジェットをビルドする
ビルドターゲットを「Widget Extension」に変更してアプリをビルドしましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643372/17d96e43-c500-26fa-3896-2ca6891b6634.png)
ウィジェットが表示されれば開発準備OKです！

## おわりに
今回は**WidgetKitで天気予報アプリ作ってみた~Widget Extension追加編~**について書きました。
次回はタイムライン作成の記事を書こうと思っています。

続きが気になる方は↓こちら↓から

https://qiita.com/nkekisasa222/items/b22c98249330f3f5c459

ご覧いただきありがとうございました。
こうしたほうがいいや、ここはちょっと違うなど気になる箇所があった場合、ご教示いただけると幸いです。

## お知らせ
現在副業でiOSアプリ開発案件を募集しています。
[Twitter DM](https://twitter.com/ken_sasaki2)でご依頼お待ちしております！
![QR_615427.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643372/4f8dff15-33fa-c042-44a3-b592f49c959c.png)
↓活動リンクはこちら↓
https://linktr.ee/sasaki.ken