iOS 14 アプリケーションのWidgetの追加（通常のWidget、Intentsを使用した構成可能なWidget)

**本記事では以下の内容を説明します。**

1. [既存のアプリケーションへのウィジェットの追加](#既存のアプリケーションへのウィジェットの追加)
2. [⭐️ ユーザーがコンテンツを構成できる (都市の選択など) ウィジェットの追加](#ユーザーがウィジェットを構成できるようにします)
3. [ウィジェットの再読み込み](#ウィジェットの再読み込み)

![ezgif-6-bf9e4b4783c9.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/1b643c98-60e5-3195-d0b1-e03a027de0c0.gif)

[完成したソースコードはこちらから確認できます。](https://github.com/mszmagic/Configurable-Widget_Example)

## 既存のアプリケーションへのウィジェットの追加

### アプリケーションターゲットの作成

既存の iOS アプリケーションへのウィジェットの追加は簡単です。ターゲット `Widget Extension` を追加してください。 

<img width="600" alt="image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/d81c538d-13fa-05b4-0957-3c152bc8fb61.png">

さて、 `Include Configuration Intent` というボックスを選択していることを確認してください。この記事のパート2では、その構成ファイルが必要になるためです。

<img width="600" alt="image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/280a43e9-a6e5-8657-2625-e54898bb9461.png">

### データ構造

これで `WidgetExample-Widget` という新しいフォルダーが表示されます。クリックしてファイル `WidgetExample_Widget.swift` を開き、そのファイルのコンテンツを削除して、このガイドに従います。

ウィジェットに表示したいデータのデータ構造を作成できます。この例では、猫の情報を表示します！

```swift
struct CatEntry: TimelineEntry {
    var date: Date
    
    var name: String
    var lastFed: Date
    var lastPlayedWith: Date
}
```

### `IntentTimelineProvider`構造の作成

`IntentTimelineProvider` 構造は、次の3種類のコンテンツを提供します：
1> `placeholder` （プレースホルダー）は、ウィジェットがロードされているときに表示されます。
2> `getSnapshot` は、ウィジェットギャラリーに表示されます。
3> `getTimeline` は、実際のウィジェット表示に使用されます。

まず、タイプ `IntentTimelineProvider` に確認するプログラム構造体を作成し、次に `Entry` のタイプを定義します。

```swift
struct CatProviderStatic: TimelineProvider {
    typealias Entry = CatEntry

    func getSnapshot(in context: Context, completion: @escaping (CatEntry) -> Void) {
        //TODO
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<CatEntry>) -> Void) {
        //TODO
    }

    func placeholder(in context: Context) -> CatEntry {
        //TODO
    }

}
```

`getSnapshot` 機能については、あなたのウィジェットが何の情報を提供するかをユーザーが理解できるよう、ウィジェットビューに例の値を提供することができます:

```swift
func getSnapshot(in context: Context, completion: @escaping (CatEntry) -> Void) {
    let entry = CatEntry(date: Date(), name: "猫の名前", lastFed: Date(), lastPlayedWith: Date())
    completion(entry)
}
```

プレースホルダービューについては、以下のように空の値または例の値を表示できます:

```swift
func placeholder(in context: Context) -> CatEntry {
    return CatEntry(date: Date(), name: "猫の名前", lastFed: Date(), lastPlayedWith: Date())
}
```

そして、タイムライン表示については、表示する実際のコンテンツを提供できます。 

この例では、ただ静的データ値を表示します。あなたのアプリケーションで、`Core Data`（[そのデータを共有する方法についての記事はこちら](https://qiita.com/MaShunzhe/items/6d13422aee5dcfaf2cc2)）、またはオンラインから、`CloudKit` から、または `UserDefaults` からのコンテンツをフェッチできます。

```swift
func getTimeline(in context: Context, completion: @escaping (Timeline<CatEntry>) -> Void) {
    let entry = CatEntry(date: Date(), name: "ネコノヒー", lastFed: Date(), lastPlayedWith: Date())
    let timeline = Timeline(entries: [entry], policy: .atEnd)
    completion(timeline)
}
```

#### タイムラインに複数のアイテムを追加する

タイムラインに複数のアイテムを追加することもできます。ウィジェットがタイムラインのアイテムを自動的にチェックし、アイテムに示された時刻になったらリロードを行います。

```swift
func getTimeline(in context: Context, completion: @escaping (Timeline<CatEntry>) -> Void) {
    var timelineEntries = [CatEntry]()
    if let date1 = Calendar.current.date(byAdding: .hour, value: 1, to: Date()) {
        let entry = CatEntry(date: date1, name: "ネコノヒー", lastFed: date1, lastPlayedWith: date1)
        timelineEntries.append(entry)
    }
    if let date2 = Calendar.current.date(byAdding: .hour, value: 2, to: Date()) {
        let entry = CatEntry(date: date2, name: "ネコノヒー", lastFed: date2, lastPlayedWith: date2)
        timelineEntries.append(entry)
    }
    let timeline = Timeline(entries: timelineEntries, policy: .atEnd)
    completion(timeline)
}
```

### ウィジェットビューのデザイン

ウィジェットの SwiftUI ビューをデザインできるようになりました。

```swift
struct CatWidgetView: View {
    
    @Environment(\.widgetFamily) var family
    
    var entry: CatEntry
    
    var body: some View {
        
        VStack {
            
            if family == .systemMedium || family == .systemLarge {
                Image("kitty")
                    .resizable()
                    .frame(width: 50, height: 50)
                    .padding(.vertical, 5)
            }
            
            Text(entry.name)
                .font(.headline)
                .padding(1)
            
            Text("最後に遊んだ時間 " + entry.lastPlayedWith.getString())
                .font(.caption)
                .padding(.horizontal)
            
            Text("最後に餌をあげた時間 " + entry.lastFed.getString())
                .font(.caption)
                .padding(.horizontal)
            
        }
        
    }
    
}
```

ウィジェットのサイズを確認するには `@Environment(\.widgetFamily) var family` 変数を使用できます。

この例では、ウィジェットのサイズが画像の表示に十分な大きさである場合の猫の画像を表示しています。

```swift
if family == .systemMedium || family == .systemLarge {
    Image("kitty")
        .resizable()
        .frame(width: 50, height: 50)
        .padding(.vertical, 5)
}
```

### ウィジェットアプリケーションをコーディングします

これで、ウィジェットアプリケーションをコーディングできるようになりました。

```swift
@main
struct CatWidget: Widget {
    
    var body: some WidgetConfiguration {
        IntentConfiguration(kind: "CatWidget", intent: ConfigurationIntent.self, provider: CatProvider()) { entry in
            CatWidgetView(entry: entry)
        }.configurationDisplayName("猫")
        .description("いつ猫に餌をあげたり遊んだりしたか見てみましょう。")
    }
    
}
```

これで、シミュレーターでアプリを実行して、設計したばかりのウィジェットを画面に追加できるようになりました。

<img width="300" alt="image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/83eba8db-1e30-da2c-d9f0-2ed0cbeff687.jpeg">

## ユーザーがウィジェットを構成できるようにします

お天気ウィジェットを使ったことがあれば、ウィジェットを長押しして構成することにより、表示される都市をユーザーが選択できることをご存じでしょう。`Intents` フレームワークとTargetを使用して、この機能を追加することができます。

### `Intents` プログラムターゲットの追加

<img width="600" alt="image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/aab04398-6109-2708-1590-9dac82cec707.png">

この段階では UI 要素は不要なため、オプション `Include UI Extension` のチェックは外して結構です。

<img width="600" alt="image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/6da8d52d-1fab-87bf-33e6-eb0289a70527.png">

作成された Intents ターゲットページで、`Supported Intents` という名前のセクションを見つけます。`ConfigurationIntent` という名前のアイテムを新規作成します。名前は `[インテント名]Intent` とします。インテント名は左側の `.intentdefinition` ファイル内に表示されます。

![スクリーンショット 0002-10-09 15.16.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/ceb58192-71bc-cc58-2747-850a82100373.png)

![スクリーンショット 0002-10-09 15.11.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/c49dd939-7893-4d94-9070-82b15ba18f8c.png)

### `.intentdefinition` の構成

次に、以前に作成したウィジェット内の `WidgetExample_Widget.intentdefinition` ファイルにつき、新しく作成された `Intents` 拡張機能をインテントのターゲットとして追加します。

<img width="300 alt="image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/a9450674-1f3b-9d1d-f8c0-69221b74e7c2.png">

画面左側の `Configuration` をクリックします。

<img width="300" alt="image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/88b6539b-5891-275c-fb34-e4189715cb73.png">

右側で、以下の画像と構成が同じであることを確認してください。

<img width="700" alt="image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/91f13c2f-f065-162f-19c4-cd9520f2a35d.png">

次に、`cat` という名の新しいパラメータを追加します。

<img width="300" alt="image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/c6c7d415-0f7c-ad5d-3570-7eacb63b463a.png">

新たに作成された `cat` パラメータの設定画面で、猫の識別子を入力として使用するため、タイプ `Type` に `String` を選択します。

<img width="600" alt="image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/449616b7-e09d-f06c-9d1d-7386b92c24b4.png">

### `IntentHandler` の構成

次に、`IntentHandler.swift` ファイルを開きます。このファイルでは、ユーザーがウィジェットの構成を行うオプションを提供します。この例では、オプションは猫の識別子となります。

クラスの `INExtension` タイプの隣に `ConfigurationIntentHandling` キーワードを追加すると、Xcode ソフトウェアが自動で次に追加すべき関数の名前を表示してくれます。

```swift
class IntentHandler: INExtension, ConfigurationIntentHandling {
    ...
}
```

この例では、完成した `IntentHandler.swift` ファイルは以下のとおりとなります。

```swift
class IntentHandler: INExtension, ConfigurationIntentHandling {
    
    
    func provideCatOptionsCollection(for intent: ConfigurationIntent, searchTerm: String?, with completion: @escaping (INObjectCollection<NSString>?, Error?) -> Void) {
        let catIdentifiers: [NSString] = [
            "ネコノヒー",
            "ムギ",
            "アズキ"
        ]
        let allCatIdentifiers = INObjectCollection(items: catIdentifiers)
        completion(allCatIdentifiers, nil)
    }
    
    override func handler(for intent: INIntent) -> Any {
        // This is the default implementation.  If you want different objects to handle different intents,
        // you can override this and return the handler you want for that particular intent.
        
        return self
    }
    
}
```

関数 `provideCatOptionsCollection` では、値のリストを入力する必要があります。これらの値は、実際には `User Defaults`、`Core Data`、オンラインのいずれかからフェッチできます。この例では値がハードコーディングされています。

[「App Extensions」で「Core Data」を使用する](https://qiita.com/MaShunzhe/items/6d13422aee5dcfaf2cc2)

### `IntentTimelineProvider` を作成

本記事のパート1では、`TimelineProvider` を使用しました。今回は、`IntentTimelineProvider` を使用します。

`IntentTimelineProvider` と `TimelineProvider` の間のデータ構造はほぼ同一です。追加の `typealias` を宣言する必要がある点が違いとなります。

```swift
typealias Intent = ConfigurationIntent
```

もう1つの違いは、次のとおりです。各関数で、インテントの選択を表す追加のパラメーター `ConfigurationIntent` を取得します。

```swift
struct CatProvider: IntentTimelineProvider {
    
    typealias Intent = ConfigurationIntent
    typealias Entry = CatEntry
    
    func placeholder(in context: Context) -> CatEntry {
        let entry = CatEntry(date: Date(), name: "", lastFed: Date(), lastPlayedWith: Date())
        return entry
    }
    
    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (CatEntry) -> Void) {
        let entry = CatEntry(date: Date(), name: "猫の名前", lastFed: Date(), lastPlayedWith: Date())
        completion(entry)
    }
    
    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<CatEntry>) -> Void) {
        let entry = CatEntry(date: Date(), name: configuration.cat ?? "", lastFed: Date(), lastPlayedWith: Date())
        let timeline = Timeline(entries: [entry], policy: .atEnd)
        completion(timeline)
    }
    
}
```

`configuration.cat` プロパティを使用して、ユーザーが選択したオプションの値を読み取ることができます。

```swift
let entry = CatEntry(date: Date(), name: configuration.cat ?? "", lastFed: Date(), lastPlayedWith: Date())
```

### `CatWidget` のコードを更新します。

パート1では `StaticConfiguration` を使用しましたが、このパートでは `IntentConfiguration`（ `Supported Intents` で設定した名前）を使用します。

```swift
@main
struct CatWidget: Widget {
    
    var body: some WidgetConfiguration {
        IntentConfiguration(kind: "CatWidget", intent: ConfigurationIntent.self, provider: CatProvider()) { entry in
            CatWidgetView(entry: entry)
        }.configurationDisplayName("猫")
        .description("いつ猫に餌をあげたり遊んだりしたか見てみましょう。")
    }
    
}
```

シミュレーターでプログラムを実行できるようになりました。ウィジェットを長押しすると `Edge Widget` というオプションが表示され、猫の名前を変更できます。

![ezgif-6-bf9e4b4783c9.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/1b643c98-60e5-3195-d0b1-e03a027de0c0.gif)

# ウィジェットの再読み込み

ウィジェット内のコンテンツに変更が加えられた場合には、メインの iOS アプリケーションでウィジェットの再読み込み機能を手動でコールすることができます。

```swift
// import WidgetKit
WidgetCenter.shared.reloadAllTimelines()
```

例えば、ToDoアプリとToDo項目の数を表示するウィジェットを持っているとします。ユーザーがToDo項目を完了または追加したときに、ウィジェットをリロードすることができます。

---

[「App Extensions」で「Core Data」を使用する](https://qiita.com/MaShunzhe/items/6d13422aee5dcfaf2cc2)

---

:relaxed: [Twitter @MszPro](https://twitter.com/MszPro)

:sunny: [私の公開されているQiita記事のリストをカテゴリー別にご覧いただけます。](http://mszpro.com/ioskiji/)
<img width="150" src="https://cdn.mszmagic.com/static-web-content/AppClipImage_small.png">