iOSエンジニアのつぶやき

毎朝8:30に iOS 関連の技術について1つぶやいています。まれに釣りについてつぶやく可能性があります。

はじめての iOS14 Widget

今回は、iOS14 の新機能として注目されている Widget について調べたことを簡単にまとめてみようかと思います。なお、本記事では Widget の実装について詳しくは記載していないので、また別の機会に書こうかと思います✍️

目次

  • 3つのポイント
  • Widget の定義
  • 考慮すべき3つの Views
  • Widget のカスタマイズと Stack でインテリジェンスを伝える方法

3つのポイント

Glanceable

ユーザは1日あたり平均で90回以上ホーム画面を見るが、そのほとんどは本の一瞬です。そのため、一目で分かるようなユーザインターフェースにすることを心がける必要があります。ボタンタップやスクロールなど複雑はユーザインターフェースは Widget の意図と反しています。

Relevant

特定の時間・ユーザに関連性を持たせた Widget を表示します。iOS14 の Widget では、スマートスタックという機能があり、ユーザのライフスタイルに合わせて適切な Widget を適切な時間にシステムが表示できるようなスマートローテーションという機能もある。

Personalization

使用しているユーザに合わせて、言語や地域を変更できるようにします。

Widget のサイズ

スクリーンショット 2020-10-19 15 34 00 参照: https://developer.apple.com/design/human-interface-guidelines/ios/system-capabilities/widgets/

iOS14 からの Widget には下記の3つのサイズが存在します。アプリは、全サイズの Widget に対応させる必要はありませんが、全サイズ対応は Apple の推奨です。

  • systemSmall
  • systemMedium
  • systemLarge

Deep Link

Widget はアプリに DeepLink させる機能を持っています、上記画像の通り、systemSmall は全体が1つのリンクとして、タップされるとアプリに遷移します。systemMediumsystemLarget に関しては、サブリンクを新しい Swift API を使用することで設定できます。

恐らくこれらのこと👇 - Sub Link の設定 - https://developer.apple.com/documentation/swiftui/view/widgeturl%28_:%29 - WidgetURL のハンドリング - https://developer.apple.com/documentation/swiftui/view/onopenurl(perform:)

Widget の定義

kind

Widget の種類を表す文字列。例えば、systemSmall の対応している Widget があるとします。その Widget の1つは、複数の企業の株価を見ることができるように設計されていて、別の種類(kind)の Widget を設定することで1つの企業の株価を詳しく見ることができる Widget を定義できるみたいな感じです。

※ kind の設定方法によって Widget のサイズが決まるということではない

configuration

Configuration には下記の2つの種類がある。

  • StaticConfiguration
    • "フィットネス" アプリのように、その日の Activity を表示するだけで、ユーザが構成を行う必要がない Widget を定義する時にこれを設定します。
  • IntentConfiguration
    • "天気" アプリのように、ユーザが構成を変更することができるようにする必要がある場合に、Widget の定義時にこれを設定します。

supportedFamilies

前述した内容と被りますが、supportedFamilies でサポートする Widget のサイズを設定します。デフォルトでは、全ての family が有効になります。

placeholder

Widget のデフォルトの UI を指します。役割としては、Widget の種類を表すことで、ユーザのデータを含むことはありません。小出しに引き出され、Widget データのロード中などに表示されます。デバイスの環境が変わる時には新しいプレースホルダー UI が求められます(例: Dinamic Type 設定が変わる時)。

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

Widget 定義のソースコード

@main
public struct SampleWidget: Widget {
    private let kind: String = "SampleWidget"

    public var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind,
                            provider: Provider(),
                            placeholder: PlaceholderView()) { entry in
                                SampleWidgetEntryView(entry: entry)
                            }
        .configurationDisplayName("My Widget")
        .description("This is an example widget")
    }
}

考慮すべき3つの Views

Placeholder

※ 前述しているので、省略

Snapshot

Widget Galley など、システムが要求した Widget を素早く表示するための View です。これには、ダミーデータではなく、実際に使用されるデータを Widget にも表示する必要があるため、extension には View を素早く返すことが求められます。

大抵の場合は、Timeline の最初の Entry と Snapshot が同じ Entry として返されます。

Timeline

Timeline は、返された View と日付の組み合わせであり、これにより特定の View をいつ出すか指示できます。タイムラインはダークとライトモード両方で返すべきとのこと。

WidgetKit extension が entry を返す時、その情報を取って View Hierarchy をディスクへシリアライズします。つまり、個々の Entry を必要になった時にレンダリングするようです。

Reloads

Reloads ではシステムが extension を呼び出し、各 Widget の新しいタイムラインを要求し、コンテンツが最新であることを確認します。

TimelineProvider への準拠

public struct Provider: TimelineProvider {
    public func snapshot(with context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date())
        completion(entry)
    }

    public func timeline(with context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        let entry = SimpleEntry(date: Date())
        let timeline = Timeline(entries: [entry, entry], policy: .atEnd)
        completion(timeline)
    }
}

Reload Policy

Reload Policy を各タイムラインに組み込むことで、次のタイムラインを要求する時をシステムに教えることができます。また、下記の3種類の Reload Policy があります。

  • atEnd
    • タイムラインの最後にリロードが行われる
  • after(date: Date)
    • 特定の日付の後にリロードが行われる
  • never
    • リロードは行われない

Widget が Timeline を返す前にユーザーのデータを求める場合などは、システムがリロードに最適な時間を決めます。また、システムはデバイス環境の変更時にリロードを強制します。

下記のようなイベントの場合は、リロードをシステムに指示する必要があります。

  • バックグランド通知
    • Widget Context 経由で WidgetKitAPI を使いタイムラインをリロードすれば、extension を呼び出すことができます。
      • 種類ごとまたは全てのタイムラインをリロード
        • reloadTimelines(ofKind:)
        • reloadAllTimelines
      • CurrentConfigurations のリストを取得
        • getCurrentConfigurations(completion:)

  • メインAppでの変更
    • Foreground App に変更を加えた場合も同様にタイムラインをリロードすることができます。

また、追加の情報をサーバに要求する場合などは、Background URL Session を使用すれば、onBackgroundURLSessionEvents の modifier を経由してペイロードが extension に送られます。また、リクエストをサーバへバッチ処理(Batch Request)し、必要な分だけネットワーキングを使用するようにします。App のプロセッサー Widget がバックグランドで動作している時、リロードはシステムに割り当てられます。

Widget のカスタマイズと Stack でインテリジェンスを伝える方法

カスタマイズとインテリジェンスには下記の2つのコンセプトがあります。

Widget を設定するメカニズムとして使われる Intent

Intent は Intents Framework によって動き、ユーザへの質問であるパラメータ式を含みます。また、Intents Framework は Siri や Shortcut との統合や、iOS14 では Widget の構成を起動するのに使用されています。また、Intents dynamic options capability を使用することで、ユーザがIntentとして設定するリストの不足を補うことができます。iOS14 では Intent が App内 Intent handling に対応しているため、メインApp が Intent の不足の質問に答えることができるとのことです。

詳細は下記 - https://developer.apple.com/videos/play/wwdc2020/10068/

Stack でインテリジェンス開発者に伝えさせる relevance

Widget の利点の1つは、Stack に複数の Widget が存在できることです。システムが最も関連性の高い Widget をローテートし、App と Widget がインテリジェンスをフィードできます。

主な方法は2つあります。

Shortcuts donation

App はユーザが操作を行う時ショートカットを寄与できます。Widget に同じ INIntent がある場合、その操作を行う時に Widget がローテーションされます。

詳細は下記 - https://developer.apple.com/videos/play/wwdc2020/10194/

TimelineEntryRelevance

TimelineEntry には TimelineEntryRelevance を使って下記の2項目で重みをつけることができます。

score

float で表され、全ての Entry の比較値として使用されます。

duration

entry が Widget に表示された時間になってから、どのくらいその score が有効であるかを示します。

こちらの記事が大変わかりやすいです👀

qiita.com

参考

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com