【iOS】（基本）Widgetをつくる前に知っておきたいこと

iOS14で進化したwidgetを開発するにあたって必要と思われる知識を簡単にまとめてみました。

## widget?

widgetとはアプリのメインコンテンツを特定の位置に配置して表示するものです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138735/2bfcf40d-6e17-9af0-257d-4a79bd2ec672.png" width="200">

widgetはiOS14でUI的にも機能的にも再設計され、よりユーザーに寄り添った経験をもたらすものとなりました。

ではwidetをつくる際にはどのようなことを知っておく必要があるでしょうか？
HIGは？
UIはどうする?
どんな機能？

まとめてみると以下の要素がありそうです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138735/fc49be54-c179-7cb7-2cfd-cb99c4f3b1d3.png" width="500">


順を追ってみていきましょう。

## コンセプト

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138735/b38b6c75-690e-9057-c3d8-ff8e18502395.png" width="200">


最初に、どんなWidgetをつくるかを考えましょう。

[HIG](https://developer.apple.com/design/human-interface-guidelines/ios/system-capabilities/widgets)には次のようにあります。

>a widget's main purpose is to display a small amount of timely, personally relevant information that people can view without opening your app.
>
>**Focus your widget on one idea.** In most cases, you can use your app's main idea as the idea for a widget. For example, a weather app can show the weather for a location, a calorie-tracking app can show calories burned for the day, and a news app can show trending stories. It can also work well to focus on one part of your app's main idea, like a game that shows a character's status or a drawing app that displays favorite sketches.
>
>翻訳
>widgetの主な目的は、ユーザーがアプリを開かなくても、タイムリーで個人的に関連性のある少量の情報をみられることです。widgetの単一のアイデアを明確にし、表示する情報をスコープすることは、設計プロセスの重要な最初のステップです。 widgetを1つのアイデアに集中させます。ほとんどの場合、アプリのメインのアイデアをwidgetのアイデアとして使用できます。たとえば、天気アプリは場所の天気を表示でき、カロリー追跡アプリはその日の消費カロリーを表示でき、ニュースアプリはトレンドストーリーを表示できます。また、キャラクターのステータスを表示するゲームや、お気に入りのスケッチを表示する描画アプリなど、アプリの主要なアイデアの一部に焦点を当てることもできます。
>

引用: [Widgets - System Capabilities - iOS - Human Interface Guidelines - Apple Developer](https://developer.apple.com/design/human-interface-guidelines/ios/system-capabilities/widgets)

アプリの機能でwidgetとしてユーザーに見せたいものを判別し、その「アイデア」をwidgetで表現しましょう。

また、widgetのゴールについて次のように語られています。

>So, to recap, the goals for a great widget experience is one that is glanceable, relevant and personalized.
>
>翻訳
つまり優れたwidgetのゴールは、一目でわかり、関連性をもち、パーソナライズされたものであるということです。

引用: [Meet WidgetKit - WWDC 2020 - Videos - Apple Developer](https://developer.apple.com/videos/play/wwdc2020/10028)

glanceable, relevant, personalized
という3つの要素が大事だとあります。

### glanceable
ユーザーはホーム画面を数秒しか見ないので、一瞥してわかるものである必要があります。

### relevant
ある場面、時間においてユーザーにとって有益な情報を表示しましょう。
例えば、appleのカレンダーアプリのように現在の時刻に基づいて最も関連性の高いものを表示するようにします。

### personalized
ユーザーに合わせてカスタマイズされたwidgetにしましょう。
widgetを編集できたり、いろいろなパターンのwidgetを提供することも時には大事です。

なんとなくwidgetで実現すべきUXがイメージできてきたでしょうか？
widgetのアイデアを考える上での方針として次のようなこともあります。

>Three useful principles we've found to focus on when thinking about ideas is to look for things that are personal, informational and contextual. 
>
>翻訳
>アイデアについて考えるときに焦点を当てることがわかった3つの有用な原則は、個人的、情報的、文脈的なものを探すことです。

引用: [Design great widgets - WWDC 2020 - Videos - Apple Developer](https://developer.apple.com/videos/play/wwdc2020/10103/)

### personal
その人固有の情報を提供しましょう。
そうすることでユーザーがアプリとより深く、感情的なつながりをもつようにできます。

### informational
適切な情報はユーザーアクションを省くことにもつながります。

### contextual
適切なタイミングで適切な情報をだしましょう。
ユーザーのニーズを察知して次のステップを提示できると素晴らしいでしょう。

## UI

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138735/adc8680c-dc24-c6ea-ff40-7218b3e4aad2.png" width="200">

詳細は [Widgets - System Capabilities - iOS - Human Interface Guidelines - Apple Developer](https://developer.apple.com/design/human-interface-guidelines/ios/system-capabilities/widgets) をみるのが良いでしょう。

ここではいくつかに絞って見ていきます。

### widgetのサイズパターン

widgetにはsmall, medium, largeの3パターンが存在します。
全てをサポートする必要はありません。特定サイズのみでもokです。

smallなwidgetには最大でも4つまでの情報とするのが望ましいです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138735/32701b33-b89f-1943-803b-a24de7a97031.png" width="300">


### レイアウト、フォント、サイズ

いくつかのお手本となるレイアウトパターンが存在します。それらを参考にするのもいいでしょう。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138735/9e2aec44-8af0-5803-fae1-0086e69cd3bc.png" width="400">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138735/f09251fa-547a-74e8-bac4-6cc100e945f4.png" width="400">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138735/34cb7ae3-8524-7d41-5eb5-60a590b4aa08.png" width="400">
引用: [Design great widgets - WWDC 2020 - Videos - Apple Developer](https://developer.apple.com/videos/play/wwdc2020/10103/)

* マージンはデフォルト16ポイント。
他のwidgetと並んだ時の一貫性を保つように。
（※ 円などのグラフィカルなものの場合は11ポイントを採用）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138735/cb32ac3c-6702-05fd-8937-f355ab2c7a84.png" width="300">

* widgetの角丸とコンテンツの角丸を一致させる。
（ContainerRelativeShapeで実現可能）

* システムフォントを使用すると、さまざまな太さ、スタイル、サイズで見栄えの良いテキストを簡単に表示できる。

* ブランドを表すためなどの理由でカスタムフォントを使用する必要がある場合は、読みやすく、他のwidgetと比べて違和感のないものになっていないか気を付ける。

* widgetの下にアプリ名が表示されるのでwidget内にはいれない。

## アクション

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138735/73ce53d3-6887-9b2b-2c67-dab3806ceeeb.png" width="200">

widgetに対していくつかのアクションを行うことができます。

### widgetの編集

widgetを長押しすることで編集メニューを表示することができます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138735/e2790a33-9949-bc19-430d-dafbf93109bb.png" width="300">

これによってwidgetのコンテンツに柔軟性を持たせることができます。
実装としてはSiriKit intent definitionファイルを追加することでこれが可能になります。

複数のwidgetを追加して、編集機能を利用して違うコンテンツを表示することも可能です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138735/0500bc15-da5e-b616-0eaf-e75a597a87ce.png" width="300">

### ディープリンク

widgetのタップでアプリにディープリンクすることが可能です。


また、widgetのsmallのタップ領域は1つです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138735/003b2991-d1de-5b26-d05f-a3c3b0e4ef46.png" width="200">

mediumやlargeの場合は複数のタップ領域を設定することができます。
固有のコンテンツはその詳細情報にディープリンクするのが良いでしょう。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138735/9f54340c-6693-adf1-dcce-905b490b9b2d.png" width="200">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138735/8567fe89-c4e2-4052-9924-e387ee70830c.png" width="200">

## コンテンツの更新

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138735/4bb2e1cb-aab1-8c29-c3c3-c5a4d6ca57a7.png" width="200">

widgetに表示しているものに対して更新をかけるにはいくつかの方法があります。

### Intent

「アクション」で見たように、SiriKit intent definitionファイルを用いた実装によりwidgetの設定項目からコンテンツの更新（変更）が可能になります。

### Timeline

WidgetKitにはTimelineという型が存在します。
そこで、TimelineEntry（エントリー。いつ、どんなコンテンツを表示するかを定義している）と次の更新タイミング（リロードポリシー）を設定することで動的なコンテンツ更新を実現することができます。

リロードポリシーは次のようなものを設定できます。

* atEnd: Timelineの最後の日付が経過した時に更新。
* after(_ date: Date): 設定した任意の日付を経過した時に更新。
* never: 新しいTimelineが設定された時に更新。「手動」更新。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138735/345e6f59-7bfe-1924-751c-c4b65c2951d7.png" width="300">
引用: [Keeping a Widget Up To Date | Apple Developer Documentation](https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date)

例えば上記の例では、最初のtimeline生成で1時間おきのエントリーが合計4つ生成され、それらが終わるとまたtimelineが更新（.atEnd）されるようになっています。そしてその後生成されるtimelineでは.neverとなっているので新たなtimelineが生成されるまではコンテンツの更新がされないようになっています。

### 更新通知

アプリ側からwidgetの更新を行うこともできます。

特定のwidgetを指定して更新する場合は次のようにします。

```.swift
WidgetCenter.shared.reloadTimelines(ofKind: "com.mygame.gamestatus")
```

すべてのwidgetを更新する場合のリクエストはこのようになります。

```.swift
WidgetCenter.shared.reloadAllTimelines()
```

引用: [WidgetCenter | Appleデベロッパドキュメント](https://developer.apple.com/documentation/widgetkit/widgetcenter)

## 関連性の設定

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138735/e97079e0-2325-a7ae-a1fd-71dfa625d0f1.png" width="200">

iOS14ではスマートスタック、というwidgetのセットを表示することができます。

> スマートスタック
ウィジェットギャラリーでは、スワイプで使えるウィジェットのセットであるスマートスタックを選べます。デバイス上の知能を使って、時間、場所、アクティビティなどの要素をもとに、適切なウィジェットを適切なタイミングで表示します。例えば、朝は天気ウィジェットでその日の天気、日中はカレンダーで次の予定、夕方はアクティビティの概要をチェックできます。

引用: [iPadOS 14 - 特長 - Apple（日本）](https://www.apple.com/jp/ipados/ipados-14/features/)

スマートスタックはユーザーに適したwidgetを表示してくれます。では、作成したtimelineがユーザーにとってどれほど重要であるか（関連性をもつか）はどのように設定すれば良いのでしょうか。

### Donation

Siri Shortcutsの実装である、donateメソッドの実行により、システムにユーザーのアクションを検知させることができます。

```.swift

.onAppear {
    let intent = DemoIntent()
    intent.task = IntentTask(identifier: task.id.uuidString, display: task.title)
    let interaction = INInteraction(intent: intent, response: nil)
    interaction.donate { error in
        // do something with error
    }

```

ユーザーが特定の時間にSiri Shortcutsで何らかのアクションを実行しdonateされることで、スマートスタックがより的確にwidgetを表示できるようになります。

### Relevance

エントリーの `TimelineEntryRelevance` 型の `relevance` を利用して各エントリーの相対的な価値を決定することができます。

```.swift

public protocol TimelineEntry {

    /// The date for WidgetKit to render a widget.
    var date: Date { get }

    /// The relevance of a widget’s content to the user.
    var relevance: TimelineEntryRelevance? { get }
}

```

例えば以下のキャラクターとそのHPなどの表示がされるwidgetでは、体力をスコアとしています。この場合、体力が大きいものを優先的に表示するようにしていることとなります。

```.swift
let relevance = TimelineEntryRelevance(score: Float(selectedCharacter.healthLevel))
let entry = SimpleEntry(date: currentDate, relevance: relevance, character: selectedCharacter)

```
引用: [Widgets Code-along, part 2: Alternate timelines - WWDC 2020 - Videos - Apple Developer](https://developer.apple.com/videos/play/wwdc2020/10035/)


## おわりに

appleのwidgetのサンプルにあるEmojiRangerのクラス図を簡単にですが作成しました。
ご参考までに。

![EmojiRanger_Widget.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138735/a31c0d5c-bd8e-2c94-df68-b6b641430c12.png)


![ios14_widget.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138735/432d83d2-92c0-1141-f490-d2e4a41ab380.png)


widgetをつくる際の参考になれば幸いです。

## 参考

* [Widgets - System Capabilities - iOS - Human Interface Guidelines - Apple Developer](https://developer.apple.com/design/human-interface-guidelines/ios/system-capabilities/widgets)
* [Meet WidgetKit - WWDC 2020 - Videos - Apple Developer](https://developer.apple.com/videos/play/wwdc2020/10028)
* [Design great widgets - WWDC 2020 - Videos - Apple Developer](https://developer.apple.com/videos/play/wwdc2020/10103/)