Open6
Widgetについて
HIG: Widgetより抜粋
- Widgetとは
- アプリやゲームのタイムリーで関連性の高い情報を少量表示し、追加コンテキストで一目で確認できるようにしたもの
- アプリを開くことなく特定の機能をロック, ホーム画面に表示し、必要な情報に素早くアクセスできる
- Widgetを編集しパーソナライズさせることもできる
- ポイント
- 驚きと喜びを与える機会を探させる
- 誕生日にレイアウトを変えたり、意味のあるものを提供
- 表示するのはシンプルな1機能であること
- ユーザーにとって最も関心の高いものを抽出
- 大きいサイズには多く情報を載せることができるが、目的を見失わないように
- 逆に小さいサイズで表示したものを大きいサイズでもただ拡大するのだけは避けること
- 表示したいコンテンツに最適なサイズを選ぶことが大事
- 逆に小さいサイズで表示したものを大きいサイズでもただ拡大するのだけは避けること
- 素早く情報にアクセスできること
- アプリのアイコンと同じような扱いになっては誰も使わない
- 動的にViewを更新しコンテンツを新鮮に保つこと
- 認証が必要で見れない場合はその旨もWidget上で伝えるようにする
- Viewの更新をうまく伝えるために最大2秒のアニメーションを実施するのもアリ
- 驚きと喜びを与える機会を探させる
- 注意
- Widgetはリアルタイムの更新をサポートしていない
- 頻繁にリアルタイムに情報を更新する機能を提供したい場合はLive Activityを利用
- システム側の都合で更新の制限を調整することがある
- 適切な更新頻度をTimelineを使い設定する必要がある
- Widgetはリアルタイムの更新をサポートしていない
WidgetKitより抜粋
- WidgetKitを使うことでWidget機能をアプリで利用することができる
- ユーザーに最新の状態をひと目で把握できるために、最新の状態を維持する必要がある
- 詳細情報が必要な場合、タップしてアプリ内の該当の場所に遷移させる
- サイズを選択し、幅広い情報をWidgetに表示できる
- ユーザーはニーズにあう表示に調整, 配置ができる
- Widgetをスタックするスマートローテーションを有効化
- WidgetKitは関連性のあるWidgetをスタックの一番上に自動的に配置
- ユーザーが適切なタイミングで重要な詳細情報を確認できる
- WidgetKitは関連性のあるWidgetをスタックの一番上に自動的に配置
- 実装
- Widget Extensionをアプリに追加
- Timeline ProviderによってWidgetを構成
- コンテンツを更新するタイミングをWidgetKitに知らせる
- SwiftUIのViewを使いコンテンツを表示
- カスタムSiriKit Intentの定義をExtensionに追加
- ユーザーがWidgetを構成できるようになる
Creating a widget extensionより抜粋
- 様々なWidgetを提供することで、ユーザーは自分にとって最も重要な情報に集中できる
- 作成の流れ
- Widget Extensionテンプレートが出発点
- 単一のWidget Extensionに複数のWidgetを含めることができる
- ただし位置情報を求めるWidgetとそうでないWidgetとでExtensionを分けて用意すると、片方で位置情報の許諾は不要にできる(後述)
- 単一のWidget Extensionに複数のWidgetを含めることができる
- Widget Extensionテンプレートが出発点
- 構成の詳細の追加
- 重要: WidgetがWidget Galleryに表示されるようにするためには、アプリインストール後にWidgetを含むアプリを少なくとも1回は起動する必要がある
- @main属性の存在
- このWidgetがWidget Extensionのエントリポイントであることを示し、ExtensionにWidgetが1つ以上あることを意味する
- Timeline Entry
- TImeline ProviderはTimeline Entryから構成されるTimelineを生成
- それぞれのEntryでコンテンツを更新する日時を指定
- またWidget Galleryに表示するためのSnapshotをTimeline Providerに求める
- プレビュー用のSnapshotをすぐ求めるため、画像生成にサーバー問い合わせなどで時間がかかる場合はサンプルデータを用意する
- Timeline Entry群と次のTimelineをリクエストするタイミングをWidgetKitに通知する再読み込みポリシーの2つで構成したものを最終的に返す
- TImeline ProviderはTimeline Entryから構成されるTimelineを生成
- Placeholder Widget
- 初回にWidgetを表示する際、ViewをPlaceholderとして描画
- ViewにはWidgetの一般的なプレゼンテーションを表示するため、ユーザーは示す内容の概要を把握できる
- View階層にあるViewをPlaceholderとして描画させないためには[unredached()](https://developer.apple.com/documentation/SwiftUI/View/unredacted]()を使う
- 初回にWidgetを表示する際、ViewをPlaceholderとして描画
- コンテンツの表示
- Widget Galleryで選択したサイズごとに色スキームなどの設定を行える
- ここで@Environment(.widgetFamily) var family: WidgetFamilyを定義した場合、systemMidiumサイズのWidgetが必須になる? -> TODO: あとで確認
- 重要: Widgetは読み取り専用の情報を提供し、スクロール要素やスイッチなどの対話型要素はサポートしていない。描画時にそれらの要素は省略される
- Widget Galleryで選択したサイズごとに色スキームなどの設定を行える
- ダイナミックコンテンツの追加
- .widgetURL修飾子を使うことでWidget押下後にDeepLinkを通し指定のアプリ画面に遷移することができる
- サイズのうちsystemMidium, systemLargeを利用する場合はLinkコントロールを利用することもできる
- .widgetURLと併用可
- App Extensionで複数のWidgetの宣言
- WidgetBundleに準拠した構造体を作り、そのbodyプロパティでWidget群をひとまとめにする構造体を宣言
- この構造体に@main属性を与えることでExtensionが複数のWidgetをサポートしていることをWidgetKitに知らせる
Keep a widget up to dateより抜粋
- Widgetが画面上にある場合でもWidget Extensionは常時アクティブでないといけない
- WidgetKitは個別のプロセスでユーザーに代わりViewを描画するため
- 常にアクティブでない状態にもかかわらず、いくつかの方法でWidgetのコンテンツを最新の状態にする方法がある
- バジェットに収まる再読み込みの計画
- WidgetKitがシステム負荷を管理するために、各自のデバイスのWidget単位で動的に割り当てリクエストする更新頻度や回数を必要分だけに制限
- Widgetの読み込みを1日を通じて分散し適用
- 午前0時ちょうどにリセットされるものではない
- ex) ユーザーが頻繁に表示するWidgetの場合
- 1日単位のバジェットに40-70回更新(15-60分起きWidgetの再読み込みが走る間隔)
- 多くの要因によりこの間隔は変化
- ex) 前回の再読み込み日時, Widgetを持つアプリがアクティブか否か, Widgetの表示頻度
- 1日単位のバジェットに40-70回更新(15-60分起きWidgetの再読み込みが走る間隔)
- システムは数日かけてユーザー行動を学習
- この期間中にWidgetで通常より多くの再読み込みが行われることがある
- バジェットにカウントしないケース
- ex) Widgetを持つアプリがフォアグラウンド, システムロケール変更, アクセシビリティの設定変更
- システム側の変更が入った際にアプリ側でTimelineの更新は行わないこと。システム側でWidget更新を実施するため
- ex) Widgetを持つアプリがフォアグラウンド, システムロケール変更, アクセシビリティの設定変更
-
Timeline Providerが再読み込みのスケジュールを主導するが、システム側が更新することもある
- ex) ユーザーがほとんど閲覧しないホーム画面のページに置いてあるWidget, 位置情報を利用するWidgetでユーザーの位置情報に大幅な変更が見られた時
- 将来の適切な再読み込みタイミングがわかる場合はできる限りその時刻をTImeline生成時に当てる
- ただし最低限5分は次の更新までに間隔をあけること
- 予測可能なイベントのTimeline生成
- 多くのWidgetではそのコンテンツが更新の意味を持つ予測可能な時点が存在
- ex) 1時間起きの更新が必要な天気, 休日の更新が不要な株式市場
- カスタムのTimeline Providerを実装し、そのTimelineを使いWidgetを更新するタイミングを追跡
- TimelineはTimelineEntryオブジェクトの配列
- 各Entryには更新日時とViewに表示するための必要な情報を持つ
- Timeline Entryの配列と合わせて新しいリクエストをするタイミングを指定(更新ポリシー)
- 更新ポリシー
- atEnd: デフォルト。配列要素の最後のTimeline Entryの日時のあとに新しいTImelineをリクエスト
- never: アプリ側でTimelineリクエストを行うまで別のTimelineのリクエストを実施しない
- after(_:): 指定のn時間後にリクエストを実施
- 重要
- 更新ポリシーでafterを利用する場合、Widgetの再読み込みでサーバー問い合わせを行う場合は計画を事前にしっかり考慮すること
- 複数のデバイスが同時刻に再読み込みを行うとサーバ負荷が大幅に増加する可能性があるため
- 更新ポリシーでafterを利用する場合、Widgetの再読み込みでサーバー問い合わせを行う場合は計画を事前にしっかり考慮すること
- 多くのWidgetではそのコンテンツが更新の意味を持つ予測可能な時点が存在
- アプリ側でTimelineの更新を通知
- WidgetCenterを使い実施
- WidgetBundleを使い複数のWidgetをサポートしている場合、WidgetCenterを使いすべてのWidgetのTimelineを更新することができる
- WidgetKitがライブで更新する時間ベースの情報をWidgetに表示することもできる
実務で体感したTips
- TODO: Testに書いた部分、全部メモリ問題のために無しになりそう(ロードによるメモリ逼迫があったのでtestableとか無視して一度WidgetExtension F/Wにブッコム
- TODO: メモリ制限の解決策として他FWtの依存を避ける。ローカライズ処理も言語を限定的にし、直接書く
メモリ
- Widget(App Extensions)は使用するメモリ量に限りがあるためそれを意識する必要がある
- メモリが逼迫しダイアログが出たりビルドエラーが発生したりする
- メモリが逼迫しダイアログが出たりビルドエラーが発生したりする
- ex) 写真アプリのアルバムから全画像取得など
-
App Extensions Programming Guide
- Optimize Efficiency and Performanceの欄参照
- メモリ容量の影響が端末環境によって異なるため、iOSバージョンが古いものやRAM容量の小さい端末での確認も出来ると望ましい
- 画像の場合 svg/pdf形式のものはビルド時にpngに変換されるのでメモリ使用量が更に増える
- ウィジェットギャラリーに表示する画像から表示に失敗することも
- TinyPNGなどを使った素材で1,2,3倍のRetina用PNG画像を用意するなどが必要
- 素材の大きさも、3倍サイズのもので500pxdp程度に抑えないと難しい
- 画像をアルバムから取得するような処理を行う場合、フェッチ処理や画像URLのリクエスト処理でメモリを大きく消費するため注意が必要
- ex) PHAsset.requestContentEditingInput(with:completionHandler)
- PHImageManagerのrequestImage()にてtargetSizeやPHImgeRequestOptionsによる設定調整を駆使してメモリ制限を回避する工夫が必要
- crop対応 -> normalizedCropRect
- 解像度を上げる -> options.isSynchronous = true
- 画像を同期的に処理するか(デフォはfalse)
- これは嘘 -> PHImageManagerMaximumSize
-
実はresizeModeは無視とAPIコメントにあるがiOS13から内部でexactが指定されている説
- なので.noneの指定が必要
- けど結局粗いまま
-
実はresizeModeは無視とAPIコメントにあるがiOS13から内部でexactが指定されている説
- これは高解像度になるがWidgetでは使えない -> asset.pixelWidth
- context.displaySize.widthを横幅に指定している以上、resizeModeとdeliveryModeをいじっても意味が無いという話もありそう(これを適用させるならpixelWidthあたりが必要になってそう)
Preview
- ViewのPreviewも作れるがサイズやTimelineごとの状態遷移もCanvasでみれるWidget用のPreviewを作って見るほうが色々みれてお得
- サイズ/Timelineごとの状態遷移
-
Preview(_:as:using:widget:timelineProvider:)
- パラメータに作成したProviderを渡しTimeline上の変化を確認
- ただしProvider内でアルバムのパーミッションやフォルダ参照などがあると厳しい
-
Preview(_:as:widget:timeline:)
- パラメータに作成したTimelineEntryを渡しTimeline上の変化を確認
- モックのEntryを直接Configurationに渡す形のためこっちのが使い勝手が良い
- サイズごとの表示の違いを確認するにはCanvas側のWidget Context Variantsで確認できる
- TimelineEntryで画像表示のためのプロパティがURLの場合、モックデータはWebから取得できるようなものの方が対応は楽
- もといImageURLをDataやImageやXCAssetsから取得するのは難しい
- Assets画像はデバイスに保存されてなくパフォーマンス上の理由で巨大なスプライトシートに合成されているのでURLで取得はできないため
- 画像をURLで取得するにはAssets外に配置し、Bundle経由でpath取得 → URL(fileURLWithPath)でやる他ない
- WidgetExtensionのようなMain Bundleでない場合のBundle指定は Bundle(for: type(of:)) のような形でAnyObjectに属するクラスを指定する必要がある
- どこにもclassを利用していないことが大半
- もといImageURLをDataやImageやXCAssetsから取得するのは難しい
サイズ調整
-
widgetFamilyを使う
- ただしsystemMediumをsupportedFamilies(:)に含んでいないと実行後にダイアログによる警告が表示される
- TODO: リジェクトされるか, 指摘のみかは要確認
テスト
- プレゼンテーションロジックらをテストしたい場合、Widget Extension Targetに対しUnit Test Bundleをincludeすることはできない
- Unit Test Bundleを新規作成しEmbed指定しようとするとその選択欄からWidget Extensionを指定できない
- WidgetUI用のFrameworkを新規作成し、Presentationに関連するViewクラスらをそこに配置する形にすると、そのFrameworkからUnit Test Bundleを作成することでテストは可能
必要なもの
- Provisioning Profile
- Widget用に必要
- Capabilitiesはアプリ側のデータを利用したりするならApp groupsの指定が必要
- (Widgetに限ったものではないが)Enterprise指定のものでBundle IDを変える必要がある場合は注意が必要
Tips
- 実はギャラリーの追加ボタンの色を変更できる
- HIG: Consider coloring the Add button.
- Assets.xcassetsにデフォルトで入っているWidgetBackgroundの色指定を変更することでできる