Swift初心者向け：WidgetKitで簡単メモウィジェットを作る方法

## はじめに

[アイスタイル Advent Calendar 2024](https://qiita.com/advent-calendar/2024/istyle)の22日目を担当させて頂きます、こたちゃんです:relaxed:
私は、新卒で入社して3年目のエンジニアです。
現在、iOSアプリのチームで保守運用を担当しております👩‍💻
今回はSwiftUI[^1]でWidgetアプリを作成します:flushed:

[^1]: SwiftUIとは、iPhoneだけでなくiPadやMacOSX、Apple Watchなど、Apple製品のプラットフォームすべてに対応しているUIフレームワークです。


## ウィジェットとは？

ウィジェットとは、ホーム画面やロック画面に小さな情報を表示する機能です。
ウィジェットは3種類のサイズ（小、中、大）があり、幅広い情報をウィジェットに表示できます。
例えば以下の画像のように「今日の予定」や「天気」などアプリを開かずに確認できます。

<img width="380" alt="widget" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/c2ba4533-ecb9-cf37-63ca-e13760730e49.jpeg">

## 作成するアプリ

<img width="280" alt="ファイル名" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/1f5a1573-569b-8509-bc8b-07962c0607fc.gif">


## このアプリ開発で学べること
- 開発環境のセットアップ方法
- SwiftUIのデザインの基礎
- WidgetKitの基本構造 

## 実装手順
[1.準備：開発環境のセットアップ](#1-準備：開発環境のセットアップ)
[2.アプリの雛形作成](#2-アプリの雛形作成)
[3.WidgetExtensionの追加](#3-WidgetExtensionの追加)
[4.ウィジェットの基本コードを理解](#4-ウィジェットの基本コードを理解)
[5.データモデルの設計](#5-データモデルの設計)
[6.TimelineEntryの修正](#6-TimelineEntryの修正)
[7.Providerの修正](#7-Providerの修正)
[8.viewの実装](#8-viewの実装)
[9.ウィジェット本体の設定](#9-ウィジェット本体の設定)
[10.プレビューの設定](#10-プレビューの設定)

## 1. 準備：開発環境のセットアップ

iOSの **ウィジェット機能** を利用するには、以下のバージョンが最低条件となります：

### **iOSバージョン**
- **iOS14** 以上  
  ウィジェットをサポートした初めてのiOSバージョン。
  ホーム画面にウィジェットを追加する機能が導入されました。
  
  参考：https://developer.apple.com/documentation/widgetkit/

### **Xcodeバージョン**
- **Xcode12** 以上  
  ウィジェットの開発を行うにはOSバージョンをサポートするXcodeのバージョンが必要です。
  iOS14向けのウィジェットを開発する場合、Xcode12以降が必要になります。
  
  ダウンロード：https://developer.apple.com/download/all/

### **補足情報**
- iOS 16以降では、ロック画面にウィジェットを追加する機能が追加され、カスタマイズの幅が広がりました。その場合、Xcode 14以降が推奨されます。
- Appleの最新技術（例：SwiftUIでのウィジェット開発）を利用する場合、XcodeとiOSの最新バージョンをダウンロードするのを推奨します。

最新のXcodeバージョンが常に全てのOSバージョンをサポートしているわけではありません。
開発中のアプリに適切な組み合わせを選んでください！

## 2. アプリの雛形作成

 Xcodeを立ち上げると「Xcode」と表示されており、その下に3つの項目があります。
※今回はXcode16.1を使用しております。

新しいiOSアプリのプロジェクトを作成するため、「Create new project...」を選択してください。

<img width="480" alt="プロジェクトを作成" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/a92dc023-65b4-5fc4-195c-4575119d2f0a.png">


iOSのAppを選択して「Next」ボタンをクリックください。

<img width="480" alt="Next" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/e2d00d19-20a0-6ddf-fb75-208cb94afd3a.png">

次にProductNameに「Memo」と入力します。
Interfaceが`SwiftUI`か確認した後に「Next」ボタンをクリックしてください。

<img width="480" alt="ProductName" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/9d7c325f-fb7d-439d-c61d-bcc48747eebd.png">

アプリファイルの保存は、ご自身のPCの好きな場所を選択し「Create」ボタンをクリックしてください。
下記のような画面になりましたら雛形作成が完了です。

<img width="480" alt="雛形" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/9e81439d-3220-2cf3-1811-6cd5336f1d97.png">

今回はウィジェットがメインのため、本体への実装は省略いたします。

## 3. WidgetExtensionの追加

プロジェクトの一番上をクリック

<img width="480" alt="target" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/d54efbab-1ed5-8dea-d25a-0529b3b48f8c.png">

プロジェクト内のTARGETSの「Memo」をクリックして、画面左下の「+」ボタンをクリックします。

<img width="480" alt="+" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/e747e7cc-c75e-8c99-dbb5-6e97a61fb9cd.png">

「Widget Extension」を選択して「Next」をクリックします。

<img width="480" alt="Widget Extension" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/2e1e6a53-1ddf-fb32-7d65-c4d397a5e2e9.png">


Product Nameに名前を入力し、「Finish」をクリックしてください。（例: `MyWidget`）

:::note to
チェックボックスが3つあるのですが今回はチェックを全て外します。
:::


<img width="480" alt="MyWidget" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/01932c73-a864-dbc6-600a-e62ca994fd93.png">

今回はチェックを外しましたがそれぞれのできることを以下にまとめています！

---

**Include Live Activity**
iOS 16.1以降で導入された新しいウィジェット機能の一部です。
ライブアクティビティを使うと、ロック画面などにリアルタイムの情報を表示できます。

**Include Control**
ウィジェットやライブアクティビティから直接操作が可能になり
アプリを開かずに便利な操作ができます。


**Include Configuration App Intent**
ウィジェットを追加する際に、ユーザー自身が設定を選択したり値の変更が可能です。

例 : 天気アプリで表示する都市を選択

---


下記のように「MyWidgetExtension」が作成されていればバッチリです。

<img width="480" alt="MyWidget" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/e91deae4-dc5e-ea55-a8bb-d0cb413421bd.png">

## 4. ウィジェットの基本コードを理解

ウィジェットの実装に入る前に基本コードの解説をしていきます。

### 構造の説明

**`Provider`** 
  ウィジェットが表示するデータを提供
**`Entry`** 
  表示するデータの構造体
**`View`** 
  ウィジェットのデザインを管理する部分でSwiftUIを使用
**`Timeline`** 
  データをどのタイミングで更新するかを管理
  
---

続いて生成されたコードの解説をしていきます。
MyWidget.swiftを開いてみると下記のように右側にシュミレータが出てきてくれています。
デフォルトのウィジェットでは時刻と絵文字が表示されています。

<img width="480" alt="MyWidget" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/d8b9f7d4-c0bf-611b-d499-ba2b220c3dcd.png">

デフォルトで下記の2つのファイルが生成されます。
※タップするとコードが確認できます

<details><summary>MyWidget</summary><div>

```swift:MyWidget
import WidgetKit
import SwiftUI

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

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), emoji: "😀")
        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, emoji: "😀")
            entries.append(entry)
        }

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

//    func relevances() async -> WidgetRelevances<Void> {
//        // Generate a list containing the contexts this widget is relevant in.
//    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let emoji: String
}

struct MyWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        VStack {
            Text("Time:")
            Text(entry.date, style: .time)

            Text("Emoji:")
            Text(entry.emoji)
        }
    }
}

struct MyWidget: Widget {
    let kind: String = "MyWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            if #available(iOS 17.0, *) {
                MyWidgetEntryView(entry: entry)
                    .containerBackground(.fill.tertiary, for: .widget)
            } else {
                MyWidgetEntryView(entry: entry)
                    .padding()
                    .background()
            }
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
}

#Preview(as: .systemSmall) {
    MyWidget()
} timeline: {
    SimpleEntry(date: .now, emoji: "😀")
    SimpleEntry(date: .now, emoji: "🤩")
}

```

</div></details>

<details><summary>MyWidgetBundle</summary><div>

```swift:MyWidgetBundle
import WidgetKit
import SwiftUI

@main
struct MyWidgetBundle: WidgetBundle {
    var body: some Widget {
        MyWidget()
    }
}
```

</div></details>

簡単にコードの解説をしていきます。

### [placeholder](https://developer.apple.com/documentation/swiftui/redactionreasons/placeholder)

ウィジェットが初めてロードされる際や、データがまだ取得できていない場合に表示されます。
空の設定をするとシステムが用意する半透明の背景が表示されます。

### [getSnapshot](https://developer.apple.com/documentation/widgetkit/timelineprovider/getsnapshot(in:completion:))

ウィジェット追加時(下記の画像)に表示する内容のデータを生成します。
<img width="300" alt="MyWidget" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/5e6978bb-c240-14b1-8ad7-8c9509a4a4ed.png">


### [getTimeline](https://developer.apple.com/documentation/widgetkit/timelineprovider/gettimeline(in:completion:))

タイムラインデータを生成します。
```swift
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
```
デフォルトは、上記のようにbyAddingに`hour`を指定しているため
現在の時刻から5時間分のエントリを1時間ごとに作成しています。
例えば、minuteだと5分のエントリを1分ごとにということになります。

```swift
Timeline(entries: entries, policy: .atEnd)
```
タイムラインの更新ポリシーは`.atEnd`を指定していますが、全部で3つあります。

- `.atEnd` : 最後のエントリの有効期限が終わると、自動で更新が行われます。  
- `.never` : タイムラインの更新は行われず、ウィジェットの内容が固定されます。  
- `.after(Date)` : 指定した日時(Date)以降に更新が行われます。

### [TimelineEntry](https://developer.apple.com/documentation/widgetkit/timelineentry)

SimpleEntryはウィジェットが表示するデータと
そのデータが表示される時間に関する情報をデータを保存しておくためのプロトコルです。
ウィジェットの内容を動的に更新するために使用されます。
ウィジェットの更新時`SwiftUIView`に渡され、ウィジェットの表示内容を決定しています。

```swift
struct SimpleEntry: TimelineEntry {
    let date: Date
    let emoji: String
}
```

### [StaticConfiguration](https://developer.apple.com/documentation/widgetkit/staticconfiguration)
ユーザがウィジェットをカスタマイズする必要が**ない**時に使用します。
この設定を使うと、ウィジェットを長押ししても「ウィジェットを編集」は表示されません。


#### 補足
`IntentConfiguration`を使用すると
ユーザーはウィジェットを編集してカスタマイズできます。
例えば下記の天気ウィジェットだと編集することで場所を変更できます。

<img width="280" alt="MyWidget" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/dd94659a-1da3-20cb-574b-25052ded3366.jpeg">


### [kind](https://developer.apple.com/documentation/mapkit/mapfeature/4232964-kind)

```swift
let kind: String = "MyWidget"
```

ウィジェットを区別するためのString型を渡します。
WidgetKitでは1つのWidgetExtensionを使用して、複数のウィジェットを作成できます。

ウィジェットをタップしてAppを起動した時、
App側でどのウィジェットから起動したかを`kind`で判別できます。

### 背景の指定

```swift
if #available(iOS 17.0, *) {
    MyWidgetEntryView(entry: entry)
        .containerBackground(.fill.tertiary, for: .widget)
} else {
    MyWidgetEntryView(entry: entry)
        .padding()
        .background()
}
```

iOSのバージョンが17以降の時は`.containerBackground`を使用し
それより前は`.background`を使用して背景スタイルを設定します。


### ウィジェットの追加画面
- configurationDisplayName
ウィジェットを設定する時に表示するタイトル文を指定します。

- description
ウィジェットを設定する時に表示する説明文を指定します。

<img width="300" alt="MyWidget" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/151bbec1-cb77-bcda-f27d-c569b062a4ff.png">

### Preview

```swift
#Preview(as: .systemSmall) {
    MyWidget()
} timeline: {
    SimpleEntry(date: .now, emoji: "😀")
    SimpleEntry(date: .now, emoji: "🤩")
}
```

Previewを使用することでXcodeのプレビュー画面でウィジェットの表示内容を確認できます。
`as: .systemSmall`はプレビューするウィジェットのサイズを指定しています。
ここでは「小サイズ（systemSmall）」のウィジェットをプレビューで表示します。

### WidgetBundle
複数のウィジェットを1つのアプリにまとめるための仕組みです。
下記のように必要なだけウィジェットを追加できます。

```swift
WidgetBundle {
    var body: some Widget {
        Widget1()
        Widget2()
        // 必要なだけウィジェットを追加
    }
}
```

:::note
今回は簡単な説明になりますので、気になる方はぜひ調べてみてください。
:::

それでは早速、メモをウィジェットに表示させていきたいと思います！

## 5. データモデルの設計

まずはモデルから作成していきます。
`MyWidget.swift`の上の方に追記する形で記述してください。

```swift:MyWidget.swift
struct Memo: Identifiable {
    let id: UUID
    let title: String
    let content: String
    let date: Date
}
```

`id` : 各メモを一意に識別するためのUUID
`title` : メモのタイトル
`content` : メモの内容
`date` : メモが作成された日付

## 6. TimelineEntryの修正

**TimelineEntry**は、ウィジェットで表示するデータの1つの「状態」を表します。
今回はメモアプリなのでemojiは不要になります。
削除して、代わりにメモのリスト (memos) を追記します。
型は最初に定義した`Memo`モデルを使用して、配列にします。

```diff_swift
struct SimpleEntry: TimelineEntry {
    let date: Date
-   let emoji: String
+   let memos: [Memo]
}
```

上記を元にサンプルデータを作成します。
今回は実際のデータを用意する代わりに、サンプルのメモデータを配列として定義しています。
実際に運用するとなるとAPIから値を取ってくる形になると思います。
定義したモデルの下に追記してください。

```swift:MyWidget.swift
let sampleMemos: [Memo] = [
    Memo(id: UUID(), title: "Shopping List", content: "Milk, Eggs, Bread, Butter", date: Date()),
    Memo(id: UUID(), title: "Meeting Notes", content: "Discuss project roadmap and timelines.", date: Date()),
    Memo(id: UUID(), title: "Ideas", content: "Start a blog about Swift development.", date: Date())
]
```

## 7. Providerの修正

SimpleEntryで`emoji`を削除したため`emoji: "😀"`は3箇所とも削除します。
そして新たに`memos`を追加します。

```diff_swift
struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
-       SimpleEntry(date: Date(), emoji: "😀")
+       SimpleEntry(date: Date(), memos: [])
    }

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
-       let entry = SimpleEntry(date: Date(), emoji: "😀")
+       let entry = SimpleEntry(date: Date(), memos: sampleMemos)
        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, emoji: "😀")
+           let entry = SimpleEntry(date: entryDate, memos: sampleMemos)
            entries.append(entry)
        }

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

補足：コメントアウトされている`relevances()`について

```swift
//    func relevances() async -> WidgetRelevances<Void> {
//        // Generate a list containing the contexts this widget is relevant in.
//    }
```

relevances メソッドは、ウィジェットの優先度を定義するために使用されます。
ウィジェットを優先的に表示したい場合（例：ロック画面での優先順位）に便利ですが、
デフォルトで生成されたコードではその機能を使用していないため、コメントアウトされています。

## 8. viewの実装

メモのように見えるようにデザインを整えていきます。
bodyの中身を以下に書き換えてくだささい。

```swift
struct MyWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            ForEach(entry.memos.prefix(2)) { memo in // 最大2つのメモを表示
                VStack(alignment: .leading, spacing: 4) {
                    Text(memo.title)
                        .font(.headline)
                        .foregroundColor(.blue)
                        .lineLimit(1)
                    Text(memo.content)
                        .font(.body)
                        .foregroundColor(.gray)
                        .lineLimit(2)
                }
                Divider()
            }
        }
        .padding()
        .background(Color.white)
    }
}
```

### [VStack](https://developer.apple.com/documentation/swiftui/vstack)
複数のビューを縦に並べるためのコンテナです。
簡単に言うと、VStackの中に入れたものが上から下へ順番に並ぶようになります。

### [ForEach](https://developer.apple.com/documentation/swiftui/foreach) と [prefix](https://developer.apple.com/documentation/swift/array/prefix(_:))
`ForEach`: 配列やコレクションの各要素に対してビューを繰り返して作成します。
`prefix`: 配列や文字列の最初の部分を取得するためのメソッドです。
()内に数字を指定してその数字分、前から要素を取得します。
範囲外の数字を入れてもクラッシュしないので安全です。

### [alignment](https://developer.apple.com/documentation/swiftui/alignment)
`alignment` は、SwiftUIでビューを配置する際に、
そのビュー内で子ビューの位置を指定するためのプロパティです。

- **`.leading`**: 左揃え（親ビューの左端に合わせて配置）
- **`.center`**: 中央揃え（親ビューの中央に配置）
- **`.trailing`**: 右揃え（親ビューの右端に合わせて配置）
- **`.top`**: 上揃え（親ビューの上端に合わせて配置）
- **`.bottom`**: 下揃え（親ビューの下端に合わせて配置）
- **`.firstTextBaseline`**: 最初のテキストのベースラインに合わせて配置
- **`.lastTextBaseline`**: 最後のテキストのベースラインに合わせて配置

複数のビューを整列させる際にレイアウトを柔軟に調整し、見た目をきれいに整えることができます。

### spacing
VStackやHStackなどのコンテナ内で、ビュー同士の間隔を設定するプロパティです。

### [font](https://developer.apple.com/documentation/swiftui/font)
テキストのフォントを指定する修飾子です。
例えば、font(.title) でタイトルフォントを適用できます。

### [foregroundColor](https://developer.apple.com/documentation/swiftui/view/foregroundcolor(_:))
テキストや文字やアイコンの色を設定する修飾子です。

### [lineLimit](https://developer.apple.com/documentation/swiftui/environmentvalues/linelimit)
テキストの行数を制限するプロパティです。
例えば、lineLimit(1) の場合は1行に制限できます。

### [Divider](https://developer.apple.com/documentation/swiftui/divider)
水平または垂直の区切り線を表示するビューになります。

### [padding](https://developer.apple.com/documentation/swiftui/view/padding(_:_:))
ビューの周囲に余白を追加する修飾子です。
例えば、padding(10) で10ポイントの余白を追加できます。

### [background](https://developer.apple.com/documentation/swiftui/view/background(_:alignment:))
ビューの背景色や背景ビューを設定する修飾子です。
例えば、background(Color.blue) の場合、青色の背景に設定できます。

## 9. ウィジェット本体の設定

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

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            if #available(iOS 17.0, *) {
                MyWidgetEntryView(entry: entry)
                    .containerBackground(.white, for: .widget)
            } else {
                MyWidgetEntryView(entry: entry)
                    .padding()
                    .background(Color.white)
            }
        }
        .configurationDisplayName("My Memos")
        .description("View your latest memos.")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}
```

iOS 17以降では、`.containerBackground(.white, for: .widget)` として白に統一し
それ以前のバージョンでは、`.background(Color.white) `を使用して白に統一しています。

`.configurationDisplayName`と`.description`は前回説明したため省略させて頂きます。
`.supportedFamilies`は、ウィジェットが対応するサイズ（ウィジェットファミリー）を指定するプロパティになります。

#### 主なファミリー
- **`.systemSmall`**: 小サイズのウィジェット  
- **`.systemMedium`**: 中サイズのウィジェット  
- **`.systemLarge`**: 大サイズのウィジェット  
- **`.accessoryCircular`**: Apple Watchの小型円形ウィジェット  
- **`.accessoryRectangular`**: Apple Watchの長方形ウィジェット  

アプリのデザインや用途に合わせて`supportedFamilies` を設定すると
そのウィジェットが対応するサイズだけを表示できます。
上記の例では、ウィジェットは小サイズ（`systemSmall`）と中サイズ（`systemMedium`）で利用可能になります。他のサイズでは表示されません。

## 10. プレビューの設定

```swift
#Preview(as: .systemMedium) {
    MyWidget()
} timeline: {
    SimpleEntry(date: .now, memos: sampleMemos)
}
```

プレビューでは、ウィジェットを中サイズ (systemMedium) で表示します。
サンプルデータ (sampleMemos) を使って表示内容を確認できます。

## 完成コード

<details><summary>Memoアプリ</summary><div>

```swift
import WidgetKit
import SwiftUI

struct Memo: Identifiable {
    let id: UUID
    let title: String
    let content: String
    let date: Date
}

let sampleMemos: [Memo] = [
    Memo(id: UUID(), title: "Shopping List", content: "Milk, Eggs, Bread, Butter", date: Date()),
    Memo(id: UUID(), title: "Meeting Notes", content: "Discuss project roadmap and timelines.", date: Date().addingTimeInterval(-3600)),
    Memo(id: UUID(), title: "Ideas", content: "Start a blog about Swift development.", date: Date().addingTimeInterval(-7200))
]

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

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), memos: sampleMemos)
        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, memos: sampleMemos)
            entries.append(entry)
        }

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

//    func relevances() async -> WidgetRelevances<Void> {
//        // Generate a list containing the contexts this widget is relevant in.
//    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let memos: [Memo]
}

struct MyWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            ForEach(entry.memos.prefix(2)) { memo in // 最大2つのメモを表示
                VStack(alignment: .leading, spacing: 4) {
                    Text(memo.title)
                        .font(.headline)
                        .foregroundColor(.blue)
                        .lineLimit(1)
                    Text(memo.content)
                        .font(.body)
                        .foregroundColor(.gray)
                        .lineLimit(2)
                }
                Divider()
            }
        }
        .padding()
        .background(Color.white)
    }
}

struct MyWidget: Widget {
    let kind: String = "MyWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            if #available(iOS 17.0, *) {
                MyWidgetEntryView(entry: entry)
                    .containerBackground(.white, for: .widget)
            } else {
                MyWidgetEntryView(entry: entry)
                    .padding()
                    .background(Color.white)
            }
        }
        .configurationDisplayName("My Memos")
        .description("View your latest memos.")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}

#Preview(as: .systemMedium) {
    MyWidget()
} timeline: {
    SimpleEntry(date: .now, memos: sampleMemos)
}

```

</div></details>

## 最後に

今年もSwiftでの記事を書かせていただきました。
Widgetの新規作成の際にぜひ参考にしていただけるとうれしいです！❤️
素敵なクリスマスを:santa_tone2::snowflake:

## 参考文献

https://developer.apple.com/jp/documentation/widgetkit/creating-a-widget-extension/

https://qiita.com/yossibank07/items/779a76f722373f60ba19