【iOS14】Widget(WidgetKit) まとめ

※この記事は2020/7/28時点のBeta3の情報を元に作成しています。

- 2020/7/25 `IntentConfiguration`の`init`でplaceholderを引数に渡すことがdeprecatedになっていたので追記しました。
- 2020/7/28 `isPlaceholder(deprecated)` `redacted` `placeholder` `unredacted` について追記しました。
- 2020/8/21 `TimelineProvider`の`placeholder(in:)`、Location Permission、 Timelineの間隔、メモリの最大容量について追記しました。
- 2020/8/27 Beta6で`TimelineProvider`のメソッド名や必須項目が変更になった件について追記しました。

iOS14から新しく登場した
WidgetKitを使ってWidgetを作成する機会があり
その際に調べたことや触ってみてわかったことをまとめました。

下記のWWDC2020のセッションを参考にしています。

Meet WidgetKit
https://developer.apple.com/videos/play/wwdc2020/10028/

Widgets Code-along, part 1: The adventure begins
https://developer.apple.com/videos/play/wwdc2020/10034/

Widgets Code-along, part 2: Alternate timelines
https://developer.apple.com/videos/play/wwdc2020/10035/

Widgets Code-along, part 3: Advancing timelines
https://developer.apple.com/videos/play/wwdc2020/10036/

Add configuration and intelligence to your widgets
https://developer.apple.com/videos/play/wwdc2020/10194/

Build SwiftUI views for widgets
https://developer.apple.com/videos/play/wwdc2020/10033/

Design great widgets
https://developer.apple.com/videos/play/wwdc2020/10103/

# Widgetとは？

iOS13では
ホーム画面から左にスワイプすると出てくる
通知センターに設定できるViewで
TodayExtensionを利用して
Viewの表示や内容の更新したり
簡単なタスクを実行することができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/dfce61d1-6c9f-1d70-a9f8-42c71cafd95e.png)

https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/Today.html

それが
iOS14ではホーム画面にも表示ができるようになり
機能もさらに豊富になりました。

iPadやMacでも
同じように表示することができます。

![widget.001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/7cb4e117-dd81-2cf2-8f5c-662907126c1e.png)


iOS14で登場したAppライブラリという機能で
アプリのアイコンをライブラリフォルダに
収納することができるようになったことで
ホーム画面をより自由にカスタマイズできるようになり
Widgetを使うことで
より必要な情報を手軽に確認できるようになりました。

![スクリーンショット 2020-07-11 11.06.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/0c8cbb47-54ac-e141-ddb8-8a42ff3c05a8.png)

# Widgetのゴール

WWDCのセッションの中で
下記の３点が
Widgetが目指すものとして掲げられています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/40a30fe7-bb37-dc18-5929-7500e188cbd3.png)

## Glanceable

パッと見てすぐに欲しい情報が確認できる

というような意味です。

https://www.dictionary.com/browse/glanceable

通常ユーザがホーム画面に止まるのは
ほんのわずかな時間です。

そのため
Widgetで何か操作を求めるなど
情報をパッと確認できること以外を行おうとするべきではない
と言っています。

Appleのアプリでは
下記のような情報が表示されるようになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/88468055-1c85-4f9a-9e70-d1b0253be8f4.png)

## Relevant

ユーザの関心があるものを適切な時間に表示する

ということのようです。

例えば

- 朝出かける前には今日の天気を知りたい
- 日中の仕事中はタスクのTodoを忘れないようにリマインダーをチェックしたい
- 寝る前にはリラックスできる音楽が聞きたい

などをWidgetでは実現することができます。

また
iPhoneの画面で
全てのWidgetを表示するのはサイズに限界があり
それを解消するために
**スマートスタック**
という新しい機能が追加されました。

### スマートスタック

Widgetのコレクションを
一つのWidgetサイズの中に納め
システムが適切な時間に適切な表示をするように
自動で表示内容を変更できる機能です。

※
自身でスワイプして表示を変えたり
自動回転をオフにすることもできます。

最終的には
システムで何を表示するかの判断はされますが
開発側で何を優先的に表示させたいのかの設定もできます。

※詳細は後ほど記載します。

### Personalize

使用しているユーザに合わせてカスタマイズできる

iOS14のWidgetでは
ユーザがWidgetに表示できる内容をカスタマイズできます。

例えば
天気アプリのWidgetで
自分が住んでいる地域の情報が知りたいとします。

そこでWidgetをタップすると
地域を選択できるリストが表示されます。

![widget6.001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/e0417c52-55e4-83e1-2337-cc7ef51462e7.png)

このように
ユーザが本当に必要としている情報が提供できる機会を
設けることが大切になります。

# WidgetKit

こういったWidgetを実現するために
WidgetKitという新しいフレームワークが登場しました。

いくつかWidgetKitのコンセプトが紹介されています。

## SwiftUIでマルチプラットホームを簡単に実現する

iOS、iPadOS、macOSどれでも同じ様に表示できるように
現時点でマルチプラットホームを実現する一番簡単な方法として
**SwiftUI**で実装するようになっています。

さらに
SwiftUIは
DynamicTypeやDarkModeへもほぼ自動で対応してくれます。

## 準備ができたら表示する

ユーザは
平均で1日90回ホームスクリーンを訪れるそうですが
そのほとんどが一瞬です。

その際に
ローディングインジケータがずらりと並んでいたら
なんの有益な情報も提供できません。

![スクリーンショット 2020-07-12 10.26.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/af39bd8c-7aa8-c719-7b2b-1b2dafadc4f4.png)

そこでWidgetKitは
バックグラウンドでViewを生成して返すようにしています。

そして
**Timeline**という一つの時間軸の上で
適切なタイミングでViewを構築して表示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/e387716b-11b8-0268-8e71-22f0cb9419bd.png)


そしてこれをフレームワーク側で管理することで
この構築したViewを他の箇所でも再利用しています。

Widgetを追加する時
WidgetGalleryと呼ばれるモーダルの中でWidgetを選択します。

そこに表示されるViewは

- 事前に用意したView

または

- すでに構築が完了しているView

が表示されます。

![スクリーンショット 2020-07-11 12.16.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/3938f95d-421b-be17-b3c8-3d24979bf94d.png)

すでに構築されたViewを利用することで
実際にホーム画面に置いた時に
Widgetがどう表示されるかを
ユーザは確認することができます。

## 情報の更新ができる

Widgetを更新するためには

- アプリからWidgetを更新する
- 定期的なWidgetの更新をTimelineに設定する

という方法があります。

例えば
カレンダーアプリのWidgetでは
予定のタイミングでWidgetの表示が更新されるように
Timelineに設定をしています。

しかし
途中で予定が変わりアプリのスケジュールを変更したタイミングで
Widgetの更新も行われ
Timelineの設定が更新されます。


![スクリーンショット 2020-07-11 12.30.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/16d63a2a-5dd8-3d1c-f4d6-ccbe340c7892.png)

![スクリーンショット 2020-07-11 12.31.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/d5a1a4ae-7af5-a689-6f40-3dffe14ffbee.png)

# WidgetKitの使い方

ここからはWidgetKitについて見ていきます。

WidgetKitには下記のようなコンセプトがあります。

- Kind
- Configuration
- SupportedFamilies
- Placeholder

## Kind

WidgetKitでは一つのextensionの中で
複数のWidgetを構築することができるようになっています。

![スクリーンショット 2020-07-11 12.40.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/188d7fed-0ef1-5fd3-c296-0f785f8540ac.png)

例えば
株価のアプリでは

- 複数の株価の概要を確認できるWidget
- 一つの株価の詳細を確認できるWidget

があります。

![widget2.001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/02fd1af3-2b4e-8cf0-86af-ea6bf2d41071.png)

## Configuration

WidgetKitには

- StaticConfiguration
- IntentConfiguration

があります。

### StaticConfiguration

ユーザがカスタマイズする必要がないものなどに使用します。

例えば
Fitnessアプリの場合
すでにパーソナライズされた情報が表示され
ユーザが設定を変更する必要がないため
StaticConfigurationを使用しているようです。

![スクリーンショット 2020-07-11 12.52.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/45eb617f-ae0a-ed19-4d94-20331a28e09a.png)

### IntentConfiguration

ユーザが設定を変更することができます。

![widget3.001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/940ae4b3-ed34-bf47-0ac6-6a1c07b1fd78.png)

Listの部分をタップすると選択可能な一覧が表示され
Widgetに表示される値の種類が変更できます。

これは**SiriKit intent definitionファイル**を定義することで
固定の選択肢を提供することができます。

さらに
iOS14の新機能
**in-app Intent handling**
を使って動的に選択肢を作成することもできます。

## SupportedFamilies

WidgetKitでは
３種類のサイズのWidgetを作成できます。

- systemSmall
- systemMedium
- systemLarge

デフォルトでは全てのサイズをサポートしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/6edc9d7d-d87c-55ea-289b-97b741caacd9.png)

## Placeholder

実装する際に必須のViewの一つで

端末の設定の変更時に利用される（文字サイズ変更など）時に
呼ばれるとセッションでは言っていましたが

ほとんど使われることはなく
**このタイミングで呼ばれるという保証はない**
とも言っています。

ポイントとしては
**このWidgetが何を表示するものなのかを表す**
ことが大事だそうです。

そして
**ユーザデータは含めずデフォルトの内容を含める**
ことが推奨されていました。

# 実装例

ここからはコードを見ていきます。

※
開発を行うにあたって
Xcode12 Beta1では
シミュレータでWidgetが利用できません（Widgetのリストに表示されない）のでご注意ください。
Beta2では表示されるようになりました。

まずはWidgetを定義します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/cde238d2-b54d-4a5f-e07a-24d8949c0959.png)

上記で紹介した
KindやPlaceholder、
実際にデータを表示するViewを提供するクロージャを引数に
Configurationを作成しています。

Providerは具体的にViewを提供する方法を定義します。
具体的には後に登場するコードをご覧ください。

下記のような構成になっています。

<img width="296" alt="WidgetKit-Architecture@2x.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/fa7c05ea-5a03-0979-9c2b-6f852433ac74.png">


## Viewを構築するときのポイント

ここから
より詳細にViewを提供するコードを見ていきますが
その前にViewを構築していく際のポイントについて見てみます。


下記の３つのWidgetは
必要最低限な情報を表示しており
タップするとアプリを開いて詳細画面へ遷移します。

![スクリーンショット 2020-07-11 14.58.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/448cfde7-6a30-14b1-6417-309ba274f32d.png)

大事なのは

**状態を持たないUI**

を構築することです。

Widgetでは
スクロールや動画の再生などの操作はできません。

これは上記でも記載した
ユーザがホーム画面にいる時間の短さなどによる特徴だと考えられます。

その代わりに
Widgetをタップすることで
アプリにDeep Linkすることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/63f250be-4b4a-47be-9e4c-95ecb31125f4.png)

`systemSmall`はWidget全体で一つのリンクになっており
タップするとアプリへ直接遷移します。

`systemMedium`、`systemLarge`では
個々のViewに`widgetURL`というメソッドを使うことで
別々のリンクを設定してアプリに遷移することができます。

https://developer.apple.com/documentation/swiftui/view/widgeturl(_:)

![スクリーンショット 2020-07-11 15.16.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/f496d780-d222-4bc4-aaa4-92596dc8f5ad.png)

![スクリーンショット 2020-07-11 15.15.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/710629fc-d8b9-0bec-9ba8-cb40750a243b.png)

そしてアプリ側では`onOpenURL`メソッドを使って
Deep Linkをハンドリングできます。

https://developer.apple.com/documentation/swiftui/view/onopenurl(perform:)


ドキュメントトップ
https://developer.apple.com/documentation/widgetkit

※
リンク先への遷移に失敗すると
アプリのトップページに遷移しました。

また
`LazyVStack`など
**Lazy**なViewと一緒に使用している場合に
画面に表示されていなければ
対象のリンク先へ遷移されません。

`View`がレンダリングされていないので
当然と言われればそうなのですが
最初「あれ？」と思ったので記録として書き残しておきます。

# Viewを提供するコード

それでは
コードを見ていきます。

Viewを提供する箇所は３つあります。

- Placeholder
- Snapshot
- Timeline

Placeholderについては
すでに見ましたので
下の２つについて見ていきます。


## Snapshot

システムが
今すぐに１つのViewを必要としている時に
提供するViewです。

![スクリーンショット 2020-07-11 15.38.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/e7a3c261-53c2-ec96-3547-1fc8fefe717b.png)

ただし
これはダミーデータではなく
実際のWidgetで利用するデータです。

なので
SnapshotとTimelineの最初のViewは
多くの場合で同じになります。

同時に
WidgetGalleyに表示されるViewになり
ユーザがWidgetを追加した時に
最初に表示されるViewにもなります。

## Timeline

複数のViewと日付の組み合わせで
いつどのViewをWidgetに表示したいか
を設定します。

![スクリーンショット 2020-07-11 15.48.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/f2642218-18a2-0a85-8de8-27cdbe94af40.png)

こうすると

フレームワークが
Viewをシリアライズ化してディスクに保存し
必要な時になったらレンダリングを行う

という仕組みになっているようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/7bc423ab-4430-c186-adeb-042eb1afa749.png)

Timelineには
一般的には1日分の内容を設定するべきだそうですが
場合によってはより更新が必要なものもあります。

その場合
**Reloads**
と呼ばれるコンセプトに基づいて
表示の更新が実行されます。

※
ドキュメントにも詳細が記載されていますのでそちらをご参照ください。
https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date

## Reloads

フレームワークが
デバイスにあるそれぞれのWidgetを起動し
新しいTimelineを提供するように要求します。

Reloadsを行うことで
ユーザに最新の情報を提供していることを保証できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/3e00bc45-1114-7a45-c124-a666bf183020.png)

※
Reloadsに関しては
ドキュメントにも詳しい記載があるのでそちらもご参照ください
https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date

具体的には`TimelineProvider`を利用します。

![スクリーンショット 2020-07-11 16.00.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/52f84351-4a05-491f-c702-85b9d8693b23.png)

https://developer.apple.com/documentation/widgetkit/timelineprovider

主に日付を持った`TimelineEntry`があり
https://developer.apple.com/documentation/widgetkit/timelineentry

`Context`にはその時のデバイスの情報などが含まれています。

- systemSmall or systemMedium or systemLarge
- Widgetのサイズや位置
- Widgetが表示される際に設定されている全てのenvironmentの情報
- WidgetがWidgetGalleryに表示されるかどうか

https://developer.apple.com/documentation/widgetkit/timelineprovidercontext


例えば
Widgetを表示するのに
外部からリソースを取得することが必要な場合
WidgetGalleryに表示されるまでに時間がかかったり
エラーが発生すると表示されないことがあります。
（その場合真っ黒な画面になります。）

そこで
`isPreview`メソッドを活用してtrueの場合には
staticなデータを返却するなどの対策を行うことができます。

#### snapshot

`snapshot`メソッドでは
1つの`TimelineEntry`を

#### timeline

`timeline`メソッドでは
複数の`TimelineEntry`に`TimelinePolicy`を添付した
`Timeline`を

返します。

下記が具体的な定義方法の例です。

![スクリーンショット 2020-07-11 16.20.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/c394b3f2-b7e9-eb58-9b98-14a396d86841.png)

### TimelinePolicy

`TimelinePolicy`には３種類あります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/52b0817a-72bd-3277-07ab-05fa9ddc6373.png)

#### atEnd

提供した最後のEntryが表示された際に
フレームワークにreloadを依頼します。

#### after(date: Date)

指定した日時に
フレームワークにreloadを依頼します。

#### never

reloadをしないようにフレームワークに依頼します。

https://developer.apple.com/documentation/widgetkit/timelinereloadpolicy


下記はドキュメントのイメージで
どのようにreloadが発生するのかが示されています。

最初に`Provider`から
3つの`TimelineEntry`を持ち
`atEnd`をRefresh Policyに設定した`Timeline`を提供しています。

そうすると
最後の`TimelineEntry`が表示されたタイミングで
フレームワークがreloadを呼ぶことで
再び`Provider`から
新しい`Timeline`を提供しています。
`never`をRefresh Policyに設定しているため
表示はずっと変わらなくなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/a37001fc-d95d-3cb1-244c-37d2d57908ad.png)


#### 注意点

ただし注意しなければいけない点としては
この設定通りには動かない可能性があります。

Reloadsは`TimelinePolicy`を基に
どのくらいWidgetが見られているかや
システムの状態を考慮して
最終的にフレームワーク側でreloadのタイミングを決めます。

また
システムの時間が急に大きく変わった場合などに
強制的にreloadする場合もあります。

※
Betaだからかもしれませんが
このタイミングはかなりバラつきがあり
どういうタイミングで発生するのかが
いまいちつかめませんでした。

(2020/8/21追記)
下記のForumの内容によると
最低でも１５分程度の間隔は空ける必要があるようです。
https://developer.apple.com/forums/thread/653265

(2020/8/27追記)
Beta6でProtocolに変更がありました。

- メソッド名が`snapshot` -> `getSnapshot`
- メソッド名が`timeline` -> `getTimeline`
- Contextの引数ラベルが`with` -> `in`
- `placeholder`メソッドが必須に


https://developer.apple.com/documentation/widgetkit/timelineprovider?changes=latest_minor


## アプリからも更新できる

上記では
フレームワークにreloadのタイミングを委ねる方法でしたが
アプリからWidgetを明示的に更新する方法もあります。

例えば
アプリがバックグラウンド通知を受け取った時や
アプリ内でユーザがデータを更新した場合などが
考えられます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/fa82298c-1656-1e5b-8f33-0b0c550cb105.png)

`WidgetCenter`を使用します。

アプリに紐づいている特定のWidgetのみを更新したり
全てのWidgetを更新することもできます。
また現在のConfigurationを知ることもできます。

![スクリーンショット 2020-07-11 16.41.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/b3bfb4d7-bd35-2532-9d20-e4196d3a9dcd.png)

https://developer.apple.com/documentation/widgetkit/widgetcenter

### 注意点

ただし何でもかんでも更新するのではなく
Widgetに表示する内容に関連する更新が行われた際にのみ
更新するように考慮する必要があると言っていました。

## バックグラウンドのURLSessionからデータを受け取る

Timelineを構築する際に
サーバからデータを取得する場合があると思います。

その際にバックグラウンドでAPIの呼び出しを行い
`onBackgroundURLSessionEvents(matching:_:)`
でリクエストの結果を受け取ることもできます。

※
ドキュメントやセッションでは言及されていますが
実際に試したところイベントを受け取ることができませんでした。
サンプルでも記載がないのでまだわかっておらず
引き続き調べています。

もしご存知の方いらっしゃいましたら
教えていただけましら幸いです🙇🏻‍♂️

https://developer.apple.com/documentation/widgetkit/intentconfiguration/onbackgroundurlsessionevents(matching:_:)-78bry

### 注意点

ただし
こちらもリクエスト数なども考慮する必要があります。

Widget用に複数のリクエストを1つにまとめるなどの
対策が必要になるかもしれません。

アプリがバックグラウンドで動いている際のreloadのタイミングは
システムに委ねられます。


## Reloadsを行う際には

プロセスやリクエストを効率的に行うこと。
Widgetは毎秒単位のずっと動いているようなものではありません。

Widgetの性質によって
どのくらいのReloadsが必要になるかを見積もり
考慮した設計をしましょう。

# PersonalizationとIntelligence

Widgetをよりユーザに使いやすくするための
主に2つのコンセプトがあります。

- Intents
- Relevance

## Intents

Intens.frameworkを利用して
ユーザにどのようにWidgetを表示するかの
選択肢を提供できます。

すでにSiriとShortcutsを一緒に利用する際に
活用されていた技術が
iOS14ではWidgetでも利用できるようになりました。

![widget3.001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/c72c7bee-e2ac-3286-69f1-39e82e812c7c.png)

※
以下で簡単に実装方法を記載していますが
詳細に関してはドキュメントをご参照ください。
https://developer.apple.com/documentation/widgetkit/making-a-configurable-widget

### Intent Definitionファイルの生成

`File > New File and select SiriKit Intent Definition File`
から作成できます。

※ドキュメントの図です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/72cd25eb-56a1-0dfc-041e-049bb26c2a70.png)

ファイルを作成すると内部で
この内容にそったクラスが自動で生成されます。
CoreDataの`xcdatamodeld`と似たような仕組みのようです。

### コード

`IntentConfiguration`と
`IntentTimelineProvider`を活用します。

![スクリーンショット 2020-07-11 17.23.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/15ee1cdc-ee25-9f83-0679-a8f9194cee5f.png)

![スクリーンショット 2020-07-11 17.25.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/6d6d306c-6acd-ca13-e615-0cf754215f42.png)

https://developer.apple.com/documentation/widgetkit/intentconfiguration
https://developer.apple.com/documentation/widgetkit/intenttimelineprovider


## Intents Extensionを使って動的に選択肢を生成する

**in-app Intent handling**
というiOS14の新機能を使って
動的に選択肢を生成することもできます。

その場合
**Intents Extension**をターゲットに追加し
`IntentHandler`を実装する必要があります。

下記のような形で実装します。

```swift

class IntentHandler: INExtension, SelectCharacterIntentHandling {
    func provideCharacterOptionsCollection(for intent: SelectCharacterIntent, with completion: @escaping (INObjectCollection<GameCharacter>?, Error?) -> Void) {

        // Iterate the available characters, creating
        // a GameCharacter for each one.
        let characters: [GameCharacter] = CharacterDetail.availableCharacters.map { character in
            let gameCharacter = GameCharacter(
                identifier: character.name,
                display: character.name
            )
            gameCharacter.name = character.name
            return gameCharacter
        }

        // Create a collection with the array of characters.
        let collection = INObjectCollection(items: characters)

        // Call the completion handler, passing the collection.
        completion(collection, nil)
    }
}
```

そして`IntentTimelineProvider`の中で
結果を使用します。

```swift

struct CharacterDetailWidget: Widget {
    var body: some WidgetConfiguration {
        IntentConfiguration(
            kind: "com.mygame.character-detail",
            intent: SelectCharacterIntent.self,
            provider: CharacterDetailProvider(),
            placeholder: CharacterPlaceholderView()
        ) { entry in
            CharacterDetailView(entry: entry)
        }
        .configurationDisplayName("Character Details")
        .description("Displays a character's health and other details")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}
```

※ 
このinitはXcode12 Beta3でdeprecatedになりました。
placeholderを引数に渡さないようです。

https://developer.apple.com/documentation/widgetkit/intentconfiguration/init(kind:intent:provider:placeholder:content:)?changes=latest_beta

```swift

struct CharacterDetailProvider: IntentTimelineProvider {
    func timeline(for configuration: SelectCharacterIntent, with context: Context, completion: @escaping (Timeline<CharacterDetailEntry>) -> ()) {
        // Access the customized properties of the intent.
        let characterDetail = lookupCharacterDetail(for: configuration.character.name)

        // Construct a timeline entry for the current date, and include the character details.
        let entry = CharacterDetailEntry(date: Date(), detail: characterDetail)

        // Create the timeline and call the completion handler. The .never reload 
        // policy indicates that the containing app will use WidgetCenter methods 
        // to reload the widget's timeline when the details change.
        let timeline = Timeline(entries: [entry], policy: .never)
        completion(timeline)
    }
}
```

~~代わりに`isPlaceholder`メソッドや
https://developer.apple.com/documentation/swiftui/view/isplaceholder(_:)~~

`redacted`メソッドと`placeholder`プロパティを組み合わせて
`Placeholder`を作成するようです。

https://developer.apple.com/documentation/swiftui/anyview/redacted(reason:)
https://developer.apple.com/documentation/widgetkit/timelineprovider/3656451-placeholder?changes=latest_beta

※ 2020/8/1追記
`isPlaceholder`はdeprecatedになっており`redacted`を使用するようです。


Appleのサンプルコード内では下記のように利用していました。

```swift

RewardsCardEntryView(entry: .init(date: Date(), points: 4))
    .previewContext(WidgetPreviewContext(family: .systemMedium))
    .redacted(reason: .placeholder)
```

さらに
`@Environment`で`redactionReason`が取得できるので
この値が空ではないとき(=現状では`.placeholder`が指定されているとき)に
placeholder表示に変更するということができます。


```swift

@Environment(\.redactionReasons) var redactionReasons
```

また逆に`unredacted`を利用することで
一部のViewのみをPlaceholder表示にしないこともできます。

https://developer.apple.com/documentation/swiftui/view/unredacted()

※ Beta3ですと`Image`はplaceholderになりませんでした。
次のアップデートで改善されるのではないかと思っています。


※ (2020/8/21追記)
~~Beta5で新しく`placeholder(in:)`が`TimelineProvider`に追加されました。~~
上記の`placeholder(in:)`はBeta3か4で追加されていたようです。
これを使用するとViewを自動で`redacted`が適用されるようです。
https://developer.apple.com/documentation/widgetkit/timelineprovider/placeholder(in:)-6ypjs

## Relevance

冒頭の方でも紹介しましたスマートスタックの中で
システムは合理的にユーザが関心が高いであろう
Widgetを表示するようにします。

その際の評価方法をアプリやWidgetを通じて
変更することができます。

主に2つの方法があります。

- アプリ内のユーザの操作からShortcutsにdonationする
- Widgetの`TimelineEntry`に`TimelineEntryRelevance`を設定する

### Shortcutsにdonation

ユーザの操作を`INIntent`として
donationしておくことで
同じ`INIntent`だと判断された際に
スタックの中のWidgetが表示される可能性が高まります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/bf21c057-8856-7dd0-ee9c-2441415cc6ef.png)

下記の例では

- 使用したカード
- 購入した商品カテゴリー

を表示するWidgetですが

月曜日と火曜日に
同じカードで同じ商品カテゴリーのものを購入したアクションを
donationしておきます。

そうすると
金曜日に同じような操作をした際に
その購入履歴のWidgetが表示されやすくなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/b986481b-dd0f-81f3-298c-a81329c13532.png)


### TimelineEntryにTimelineEntryRelevanceを設定

各`TimelineEntry`には
`TimelineEntryRelevance`を使って
重みをつけることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/7c048bea-d39c-56a1-0f96-fd22f416e7b3.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/d9dacc8a-050c-a85c-657b-3a116bd3ba71.png)

https://developer.apple.com/documentation/widgetkit/timelineentryrelevance

`score`は`float`で
これまでに提供した**全てのEntry**との比較値として利用されます。

ドキュメントにも下記のような記載があります。

```
The score is a value 
you choose that indicates the relevance of the widget, 
relative to entries across timelines that the provider creates.
```

例えば
下記のように購入した金額を`score`に設定することで
購入金額の高いものが優先的に表示されるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/acbe8ca7-2d23-45a2-1a8e-51ca37e841a9.png)


`duration`は
設定されたEntryが
Widgetに表示する時間になってから
どのくらいその`score`が有効であるかを示す値です。

例えば
下記の例はバスケットボールの試合のスコアを表示するWidgetです。

試合の開始時に`score`加え
試合が終了するまでの3時間(`duration = 3hrs`)を設定しています。
以降がnil(設定なし)なのは
Relevanceに影響を与えないことを示しています。

一つ前で0としているのは
次のRelevanceが来るまでは
この`score`(= 0)を継続して使用することを示しています。

![スクリーンショット 2020-07-11 18.14.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/ea81e85e-1117-d64d-aad4-c659f93a7a47.png)

# 良いWidgetを設計するために

ここからは良いWidgetを設計するためのポイントを
セッションの内容から見てみたいと思います。

Design great widgets(優れたWidgetの設計)のセッションの中で
大きく2つに分けて紹介されていました。

- Ideation(何を表示するのか)
- Creation(どう表示するのか)

※
Creationに関しては
HIG(Human Interface Guideline)と内容が似ていましたので
ドキュメントをご参照して頂けましたら幸いです。

https://developer.apple.com/design/human-interface-guidelines/ios/system-capabilities/widgets

## Ideation

大きく分けて3つについて
述べられています。

- Principles
- Editing
- Multiples

### Principles

大事なポイントとして以下の３点が挙げられています。

![スクリーンショット 2020-07-12 5.11.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/baf30ac8-b2c9-c204-ab27-6369f519a7ee.png)

#### Personal

よりユーザに特化した情報を提供することで
アプリとユーザの感情的な繋がりを強くする。

#### Information

多くの情報の中から
一番重要な情報の概要を選んで提供する。

#### Contextual

ユーザがアプリで繰り返し操作して得ているような情報(必要としている情報)
を提供する。

必要な情報は時と場合によって異なるので
それに応じた情報が提供できるようにする。

### 例

カレンダーアプリは下記のようにスケジュールに応じて
表示方法を変えています。

一番重要な情報として
直近のスケジュールの内容と時刻と場所を表示します。

また
スケジュールの量や祝日に合わせて表示方法を変えたり
連絡先から知り合いの誕生日を表示することができます。

![widget4.001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/c247eab9-f6c7-1498-c41b-3986861cdcd2.png)

天気アプリでは
例えば雷雨が発生している場合は
より細かい分ごとの降水量を表示して
何時ごろに雨が止むのかが確認できるようになります。

![widget5.001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/6577d2d5-8595-6b4a-b08b-3785aaf4d125.png)

### Editing

iOS14の新機能で
ユーザがWidgetに設定できる内容を
カスタマイズできます。

これはWidgetにどのように情報を表示するのかに
関わってきます。

冒頭の方でも紹介しましたが
天気アプリでは表示する地域を選択できます。

![widget6.001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/e0417c52-55e4-83e1-2337-cc7ef51462e7.png)

さらに同じ種類の複数のWidgetを表示して
他の地域の天気を表示することもできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/9726ff48-0772-83f6-7a2b-6b39e1d3eff0.png)

これが実現できることで
1つのWidgetに複雑な情報を詰め込む必要はなく
シンプルでわかりやすい表示を可能にします。

### Multiples

1つのアプリの中で
複数の種類のWidgetを作成することも可能です。

例えば
株価アプリでは
ウォッチリストのサマリーを表示するWidgetと
一つの株価を細かく表示するWidgetが
用意されています。

どういった情報をユーザがすぐにチェックしたいかどうかを考え
複数のWidgetを用意するべきかどうかを検討します。

![スクリーンショット 2020-07-12 9.07.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/509e013a-8351-71a0-5cc2-4870b5da1cec.png)


# Widget作成に役立つView

最後にセッションの中で紹介されていた
Widget作成において役に立つViewを2つ紹介します。

## Text init(_:style:)

SwiftUIのテキストに新しいイニシャライザが追加され
DateからTextを生成できるようになりました。

```swift

init(_ date: Date, style: Text.DateStyle)
```

これは指定した時間からの経過時間を自動で表示してくれます。
Widgetは常時動いているものではありませんが
このTextを活用することで
リアルタイムに起動しているように見せることができます。

※セッションの画像です。

![スクリーンショット 2020-07-12 9.44.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/5a99681b-0c44-aea6-81cc-13f313931d23.png)

またString Interpolationを活用することで
表示のローカライズも可能です。

![スクリーンショット 2020-07-12 9.45.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/ce7fef5a-cb0d-47c8-806e-0cf9fc0bae28.png)


https://developer.apple.com/documentation/swiftui/text/init(_:style:)

## ContainerRelativeShape 

HIGにも記載されていますが
WidgetのCorner Radiusと
コンテンツのCorner Radiusは
一致させることが推奨されています。

しかし
この値はデバイスサイズに変わってくるため
手動で計算するのは大変難しいです。

![スクリーンショット 2020-07-12 9.49.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/1101bc4f-685f-6ad0-346d-fb4f65359bdc.png)


そこで今回
`ContainerRelativeShape`が追加されました。

これは親ViewのCorner Radiusに合わせるように
フレームワーク側で自動で計算してくれるようになります。

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

# Beta5よりLocation Permissionが必要に
(2020/8/21追記)

WidgetでLocationを使用する場合に
Permissionをユーザに求めることが必要になりました。

そのため
Info.plistに下記のKeyが必要になります。

- `NSLocationUsageDescription`
- `NSWidgetWantsLocation`

`NSWidgetWantsLocation`
https://developer.apple.com/documentation/bundleresources/information_property_list/nswidgetwantslocation

# メモリの最大容量

ドキュメントに記載はありませんが
こちらの記事によると30MBを超えるとクラッシュするようです。

https://medium.com/swlh/10-tips-on-developing-ios-14-widgets-f17b865fbdbc

# まとめ

iOS14のWidgetについて見てきました。
これまでのアプリのアイコンが並んだホーム画面から
よりユーザが必要としている情報をすぐに確認できるようになるのは
便利そうだなと感じています。

一方で
必要ないものはユーザからどんどん遠ざかっていくのではないかとも感じており
Widgetを使うことでユーザに利便性を高めていくことは
大事になってくるのだろうと思いました。

まだまだ不明な点が多く
実際にReloadの（`timeline`メソッドが呼ばれる）タイミングで
通知音を出して確認してみましたが
予定の時間に更新がされなかったり
予期せぬタイミングで更新されたりします。

※
ロック画面から復帰したら鳴ったり
他のアプリ削除したら鳴ったり
ウィジェットを追加していなくても鳴ったり...など

ただ使い続けていると
Timelineに設定した内容に関しては
だんだん時間通りに通知が来るようになってきたなとも感じています。
アプリの使用頻度も関わっているようなので
それが影響しているのかなと思っています。

データの更新とViewの更新のタイミングも
必ずしも一致しているわけではありませんでした。

また
スマートスタックを利用した時に
どのくらいの頻度で自分のWidgetが表示されるのか
Relevanceの有効性がどのくらいなのかなど
まだまだわからないことがあるので
これからも触ってみて色々調べていきたいなと思います😃

何か間違いなどございましたら
教えていただけますと助かります🙇🏻‍♂️