テクノロジー

2020.12.19

ドキュメントには書いていないWidgetKit Tips集

Yahoo! JAPAN Advent Calendar 2020の19日目の記事です。

こんにちは、iOSアプリ黒帯(ヤフー内のスキル任命制度)の林です。
9月にiOS14がリリースされ、その目玉の一つとしてウィジェットという機能が利用可能になりました。

ウィジェットはアプリの一部情報をiOSのホーム画面へ表示させられる機能です。ヤフーではiOS 14のリリースに合わせて複数のアプリがこの機能をサポートしました。

例えば、Yahoo! JAPANアプリではニュース・天気・検索といった主要機能をウィジェットとして提供しています。

Yahoo! JAPANアプリのウィジェット画面

ウィジェットは「WidgetKit」という専用フレームワークを使ってイベント管理します。UIの構築には「SwiftUI」を使用します。WidgetKitは独特なアーキテクチャとなっており、アプリ本体とは作り方が異なります。一定の時間間隔で決まった関数を呼び出すという形式が取られていて、そのタイミングで必要なデータ処理とUI生成を一括で行う必要があります。

簡単な図を使って説明すると、以下のようなイメージです。TimelineProviderと呼ばれるプロトコルのメソッドが一定ルールで呼ばれ、TimelineEntryを複数個生成します。 TimelineEntryには更新後の画面へ表示させるデータを渡します。それぞれのTimelineEntryはおおよそ指定時間になったらフックされ、それによってWidgetのUIが更新されるようになっています。 WidgetKitのアーキテクチャ

ウィジェットはWWDC20で発表された当初から注目されていたため、この辺りの詳細な技術情報は既に目にされている方も多いでしょう。さらに詳しく知りたい方は以下のDevelopment Guideや動画を御覧ください。

この記事では実際にウィジェットをリリースした経験を元に、試行錯誤する中で知ったドキュメントからは読み取れない細かいTipsをまとめていきたいと思います。

また、環境は執筆時点で最新のXcode 12.2, Swift 5.3.1を想定しています。

ウィジェット開発 Tips集

UIKitのAPIは全く使えないというわけではない

ウィジェットを開発する上でこれが一番気になるところかもしれません。UI実装に使うSwiftUIは2019年から利用可能になったまだまだ発展途上のフレームワークです。そのため古くから使われているUIKitのようにさまざまなユースケースへ対応できるようにはなっていません。

WWDC20の動画を見るとUI実装はSwiftUIで行うと話されており、実際にUIViewやUIViewControllerをUIViewRepresentation経由で利用するとエラーになります。

// こういったコードでUIViewを表示させることはできない
import UIKit
import SwiftUI

struct HogeView: UIViewRepresentable {
    func makeUIView(context: Context) -> UIView {
        return UIView()
    }

    func updateUIView(_ uiView: UIView, context: Context) {}
}

そのため、残念ながら既にアプリで利用しているUIKitベースのUIコードを使い回すなどは難しいでしょう。

しかし、UIKit自体を使えないかというとそういうわけではありません。UIKitには描画用のAPI以外のものもあり、例えば実行環境のデバイス情報を取得できるUIDeviceなども含まれています。このクラスは利用することができ、例えばiPhoneとiPadでUIを出し分けたいといった要件に活用できます。

UIDevice.current.userInterfaceIdiom

「UIKitは使えない」とおぼえてしまうと見落としてしまう点なのでご注意ください。

iOS14以降でしか使えない最新APIを気にせずに使える

アプリにとって重要なビジネスロジックはフレームワーク化するなり、ターゲットを追加するなど共通化しておいたほうがいい場合もあるでしょう。ただ、重複しているけれどもそこまでするのはコストパフォーマンスが悪い微妙なロジックもあるでしょう。そういったコードの場合は、アプリ内の既存コードを参考にしつつ重複コードを書くのがいいでしょうか?

ウィジェットはiOS14以降のサポートを強制されるため、最新のAPIをあえて使って慣れておくことをおすすめします。具体的なものでいうと、例えばCombine FrameworkのAPIは互換性を気にせず使えます。ウィジェットではURLSessionのdataTaskをそのまま利用できるのですが、Combine用のAPIも用意されています。

細かい話ですが、そもそも使えるということを忘れていたり、保守的になってつい昔ながらの書き方をしたりするケースもあると思うので、頭の片隅にいれておくといいと思います。

非同期な画像取得やUIの更新はできない

SwiftUIと聞いたときに真っ先に思い浮かべるのは、StateやObservableObjectを使ったデータバインディングかもしれません。

ウィジェットは前述した特徴のアーキテクチャを持つため、データの処理やそれによる画面の更新を1カ所で行う必要があります。したがって、実はStateやObservableObjectを有効活用してView単位で個別に画面更新を行うということができません。最初に使える前提でクラス設計をしてしまうかもしれませんが、そこは注意が必要です。

では、URLからの画像の取得はどのようにやればいいでしょうか。アプリ開発では画像の表示領域が現れたときのイベントで非同期に取得するのが通常のやり方でしたが、ウィジェットではシンプルに画面更新前に必要な画像を取得しておく必要があります。

APIのレスポンスに画像URLがあればData型を使って取得しておき、必要なものが全て取れたら画面のレンダリングを開始します。

try Data(contentsOf: url)

どうしても画面のイベントで処理を走らせたい場合は、Secure App GroupsとDownload Taskを組み合わせる

前述したとおり、Viewのイベント単位で処理を走らせることができない仕様です。とはいえ、どうしても画面が表示された任意のタイミングで通信を走らせたいといった要件があるかもしれません。

ウィジェットではURLSessionのdownloadTaskによって例外的にバックグラウンドの通信を行えます。

このdownloadTaskは画面表示のタイミングでも呼び出すことができ、同じく利用可能なApp Groupsの機能と組み合わせることで、いったんUserDefatulsなどに保存したデータを使って通信を行えます。

この組み合わせで、上記の要件を満たすような実装が可能かもしれません。

Keychain Sharingは使える

基本的にアプリと共有するデータとしては上記のSecure App Groupsを使わうことになるでしょう。それ以外にもKeychain Sharingが利用可能です。特に明示はされていないのですが、Xcode上のCapabilityでは選択できるようになっています。

実際にentitlementsを追加してみると、同一ベンダーのKeychainデータを共有できました。Keychainのセキュア領域にもともとデータを保存しているアプリではこの機能を活用できます。

メモリ上に展開できるデータはトータルで30MB以内に収める必要がある

ウィジェットはApp Extensionのひとつです。App Extensionの開発経験がある方はウィジェットで扱えるデータ量には制限があるのでは? と想像されているかもしれません。そこに関してはどうやらその通りで、トータルで最大30MB以上のデータはロードしておけないという報告があります。

例えば通信によって画像を大量に扱う場合など注意が必要でしょう。

ギャラリーは表示が保障されているデータを出したほうがよい

iOS14にはギャラリーというウィジェット選択画面があります。表示されたときにイベントが呼ばれるようになっており、それを通してUIにデータを渡します。

基本的には時間ごとに呼ばれるイベントのときと同じように実装すればいいのですが、以下の仕様に注意する必要があります。

In that case, call the completion handler as quickly as possible, perhaps supplying sample data if it could take more than a few seconds to fetch or calculate the widget’s current state.

具体的にどのくらい短いかですが、手元で試してみる限りでは通信は問題なくできており、大きく違いはないように見えました。ただし、何度も繰り返し試しているとまれに通信に失敗するケースがありました。アプリによって取るべき実装は変わってくると思いますが、単純にイメージ画像などが表示されていればいい場合はモック用のデータをウィジェット内に持って表示させる形でもいいかと思います。

この時前述の30MB制限は意識して、画像サイズなどを検討する必要があります。

PreviewのコードはProductionには入らない

SwiftUIを使うため、開発中はXcodeのPreview機能をよく使うことになるでしょう。Previewを使うとSwiftUIで実装したコードの修整とそのUIのレンダリングがリアルタイムで同期させられます。

何かしらの動的なデータを表示させる場合、Preview内ではモックデータを使うケースがほとんどだと思います。

struct Widget_Previews: PreviewProvider {
    // Mockデータを用意する
    static var placeholder: HogeEntry {
        HogeEntry(
            date: Date(),
            dataSource: "ほげ")
    }

    static var previews: some View {
        return HogeWidgetEntryView(entry: placeholder)
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

Previewのおかげで手軽にUIの修整ができて便利なのですが、このPreviewへ渡しているデータはバイナリに含まれてしまわないだろうかと心配される方もいるかもしれません。

以前は#if DEBUGで直接囲むコードが生成されていました。現在はこの形でのテンプレート生成がされません。しかし、コンパイラの機能である「dead code elimination」で最適化時に削除される仕様になっています。

そのため、直接渡すのであれば心配する必要はなさそうです。また、上記のフォーラムでも書かれている通り、本番環境で不要リソース類は「Development Assets」へ入れるのがいいでしょう。

まとめ

いかがでしょうか。

言われてみれば当たり前の内容も多いのですが、はじめからそういうもんだとわかって開発できるとある程度工数も違ってきます。

これからウィジェットを開発される方のお役に立てると幸いです。


林 和弘
iOSアプリ黒帯
Yahoo! JAPANアプリの開発をしながらiOSアプリ黒帯活動もしているソフトウェア開発者

Yahoo! JAPANでは情報技術を駆使して人々や社会の課題を一緒に解決していける方を募集しています。詳しくは採用情報をご覧ください。

関連記事

このページの先頭へ