Expo のホーム画面ウィジェットとライブアクティビティ
新しい expo-widgets ライブラリを使えば、ネイティブ設定なしで Expo UI コンポーネントからホーム画面ウィジェットやライブアクティビティを構築できる。
日本語
コピー

ホーム画面ウィジェットとLive Activityは、iOSアプリが提供できる最も目立つ機能のひとつだ。うまく作られたウィジェットは、アプリを閉じている間もユーザーの生活の中にアプリを留め、継続率とエンゲージメントを押し上げる。競争の激しいApp Storeでは、この点がますます重要になっている。
つい最近まで、React Nativeアプリからウィジェットを出すにはネイティブの作業が山ほど必要だった。別のXcodeターゲット、データ共有のためのApp Groups、SwiftUIのレイアウトコード、そして拡張機能とアプリ本体の残りの部分を同期させ続けるという長期的な負担だ。
この記事ではExpo Widgetsのalpha版を紹介する。Expo UIコンポーネントでホーム画面ウィジェットとLive Activityを構築できる、待望の新しいライブラリで、ネイティブ側の設定はContinuous Native Generationが処理してくれる。
ウィジェットとLive Activityとは?
ウィジェットは、ホーム画面やロック画面に置かれる小さく一瞥できるUIだ。アプリがリアルタイムに描画するのではなく、システムがスケジュールに従ってレンダリングする。ウィジェットからアプリの特定画面へディープリンクでき、iOS 17以降はボタンやトグルなどのインタラクションにも対応する。
Live Activityは、ロック画面とDynamic Islandに表示される期間限定の更新だ。配達、試合、移動などイベントの開始時にアプリがLive Activityを開始し、その後は状態の変化に合わせて更新する。更新はアプリ自身からでも、APNsのプッシュ通知経由でもよい。イベントが終わればLive Activityは終了する。
ウィジェットをReactへ
Evan Baconのexpo-apple-targetsはXcodeの設定を自動化したが、ネイティブコンポーネントは依然としてSwiftUIで書く必要があった。
expo-widgetsはReactコンポーネントでウィジェットを定義できるようにする。config pluginがWidget Extensionターゲットを生成し、App Groupを設定し、prebuild時に必要なファイルをすべて作成する。
これを可能にしている鍵が@expo/uiだ。ウィジェット拡張はレンダリング時にReact Nativeを動かせない。システムがオンデマンドでレンダリングし、時間の予算は厳しく、アプリが起動していない状態で動くことさえある。レイアウトはネイティブの形で表現しなければならない。
@expo/uiはText、VStack、HStack、ImageといったReactコンポーネントを公開しており、これらはSwiftUIのネイティブコンポーネントと一対一で対応する。システムがウィジェットのタイムラインを要求すると、あなたのコンポーネントが独立したJSランタイムで実行され、@expo/uiのレイアウトツリーを生成する。その後ネイティブ側がその記述をもとにSwiftUIビューでUIを再構築する。実際のレンダリングにReact Nativeは一切関与しない。
Widget
Widgetはごく普通のReactコンポーネントだが、いくつか重要な制約がある。
-
自己完結していること
-
widgetディレクティブでマークすること -
表示データはpropsで渡す(加えてwidget固有のフィールドがいくつか)
データはpropsとして渡され、その中には'family'などの追加フィールドが含まれる。どのサイズが埋められているかを示すもので、サイズごとにwidgetを定義しなくてもレイアウトを適応させられる。あとはデータと対応するタイムラインをあらかじめ並べておけば、システムが正しいものを自動的に表示する。

Live Activity
Live Activityも同じ考え方に従う(Expo UIでレイアウトを記述し、propsをプッシュして更新する)が、レイアウトには表示されうる場所に対応する複数の「スロット」がある。
-
ロック画面バナー
-
Dynamic Islandコンパクトモード(leading + trailing)
-
Dynamic Island最小モード
-
Dynamic Island展開モード(leading、center、trailing、bottom)
APNs経由でサーバーから直接更新をプッシュすることもでき、push-to-startトークンを使えばユーザーの操作なしにリモートでactivityを開始することもできる。

最初のWidgetを作る
ライブラリをインストールし、最初のwidgetを設定したあと、最小のwidgetはこうなる。コーヒーの杯数を記録し、ユーザーがホーム画面から直接1杯追加できる:
import { Button, Text, VStack } from '@expo/ui/swift-ui';
import { font, foregroundStyle } from '@expo/ui/swift-ui/modifiers';
import { createWidget, WidgetBase } from 'expo-widgets';
type Props = { count: number };
const CoffeeCounter = (p: WidgetBase<Props>) => {
'widget';
return (
<VStack spacing={8}>
<Text modifiers={[font({ size: 48 })]}>☕</Text>
<Text modifiers={[font({ size: 32, weight: 'bold' })]}>{p.count}</Text>
<Button
modifiers={[foregroundStyle('white')]}
label="+"
target="increment"
onPress={() => ({ count: p.count + 1 })}
/>
</VStack>
);
};
export default createWidget('CoffeeCounter', CoffeeCounter);
関数本体の先頭にある'widget'ディレクティブがこれをwidgetとしてマークする。onPressはローカル状態の更新を返す。現在の状態にマージされ、再レンダリングされる。アプリを起動する必要はない。
WidgetとLive Activityはどんなときに使うか
ユーザーが一日のうちに一瞥して得をするものは、たいていwidgetに向いている。これから始まる予定、現在の天気、タスクの件数、習慣トラッカーの連続日数などだ。
Live Activityが最も向いているのは、終わりのある進行中のイベントだ。フードデリバリーの配達予定までのカウントダウン、試合時間付きのライブスコア、フライトのステータス更新、配車ドライバーの位置、ワークアウトのタイマーなどが挙げられる。ポイントは、自然な開始のタイミングと自然な終了のタイミングがあることだ。
覚えておきたい原則がひとつある。ウィジェットは「状態を見る」ため、Live Activityは「進行中」のためのものだ。
今後の予定
いくつかを追加する予定だ:
-
タイムラインの再読み込み——アプリを閉じている間に適用されたタイムライン更新をすべてアプリから取得し、状態をアプリのロジックに同期し直せます。
-
更新ポリシー——タイムラインをスケジュールするときに更新ポリシーを指定し、WidgetKit が次にいつ新しいタイムラインを要求するかを伝えられます。たとえば最後のエントリを表示し終えたタイミングや、特定の日付などです。
-
画像——ExpoUI は現時点で画像の表示に対応していません。
expo-widgets 現在は alpha 段階。フィードバックを集め、粗い部分を整えていく中で、一部の API は変わる可能性があります。
あなたが何を作るのか楽しみにしているし、意見も聞きたいです。動かない箇所や欲しい機能があれば、教えてほしい。