概要
新しい expo-widgets ライブラリにより、ネイティブのセットアップを一切行わずに、Expo UI コンポーネントでホーム画面ウィジェットと Live Activities を構築できます。ホーム画面ウィジェットと Live Activities は、iOS アプリが提供できる最も目立つ機能の一部です。よく作られたウィジェットは、アプリが終了していてもユーザーの生活に常に存在感を与え、ユーザー維持やエンゲージメントを改善します。競争の激しい App Store ではこれはますます重要です。
これまで、React Native アプリからウィジェットを出荷するには多くのネイティブの手続きが必要でした: 別の Xcode ターゲット、データ共有のための App Groups、SwiftUI のレイアウトコード、そして拡張機能をアプリ本体と同期させ続ける負担などです。
本記事では、Expo Widgets(アルファ)を紹介します。これは Expo UI コンポーネントでウィジェットと Live Activities を定義でき、Continuous Native Generation がネイティブ設定を自動で処理してくれる新しいライブラリです。
ウィジェットと Live Activities とは
- ウィジェットはホーム画面やロック画面に配置される小さな「ひと目で分かる」UI です。システムがスケジュールに沿って描画し、アプリがリアルタイムに描画するわけではありません。ウィジェットからアプリの特定部分へディープリンクできます。
- iOS 17 以降、ウィジェットはボタンやトグルによるインタラクティブ操作も可能になりました。
- Live Activities はロック画面や Dynamic Island に表示される時間制限のある更新表示です。配達、試合、乗車などイベント開始時にアプリが開始し、アプリや APNs を介したプッシュで更新します。イベント終了時にアクティビティは終了します。
React への適用
Evan Bacon の expo-apple-targets は Xcode のセットアップを自動化しましたが、それでも SwiftUI でネイティブコンポーネントを書く必要がありました。expo-widgets はウィジェットを React コンポーネントとして定義できるようにします。config plugin が Widget Extension ターゲットを生成し、App Group を設定し、prebuild 時に必要なファイルを作成します。
この仕組みを可能にする鍵は @expo/ui です。ウィジェット拡張はレンダリング時に React Native を動かせないため、システムが要求するタイムラインに応じてネイティブ側で SwiftUI レイアウトに変換できる記述が必要です。@expo/ui は Text、VStack、HStack、Image といった React コンポーネントを公開し、これらは SwiftUI のプリミティブに直接対応します。
システムがウィジェットタイムラインを要求すると、あなたのコンポーネントは別の JS ランタイム上で実行されて @expo/ui のレイアウトツリーを生成します。ネイティブ側はその記述を受け取り、SwiftUI ビューを組み立てて UI を再構成します — 実際のレンダリングには React Native は関与しません。
ウィジェット(Widgets)
ウィジェットは通常の React コンポーネントですが、いくつか重要な制約があります:
- 自己完結している必要がある
- 関数本体の先頭で
"widget" ディレクティブでマークする必要がある
- 表示データを props として受け取る(さらにウィジェット固有のフィールドがいくつか付与される)
ウィジェットは family のようなフィールドでどのサイズの領域を埋めているかを受け取るため、サイズごとに別ウィジェットを定義せずにレイアウトを調整できます。事前にデータでタイムラインをスケジュールしておけば、システムが適切なエントリを自動的に表示します。
Live Activities
Live Activities は同じ考え方(Expo UI でレイアウトを記述し、props をプッシュして更新)に従いますが、表示されうる「スロット」が複数あります:
- Lock Screen banner
- Dynamic Island compact(leading + trailing)
- Dynamic Island minimal
- Dynamic Island expanded(leading、center、trailing、bottom)
さらに、サーバーから APNs 経由で直接プッシュ更新を送ることや、push-to-start トークンを使ってユーザーの操作なしにリモートでアクティビティを開始することも可能です。
最初の Widget を作る方法
ライブラリをインストールして最初のウィジェットを設定したら、コーヒーのカウントを追跡し、ホーム画面から直接増やせる最小のウィジェットは次のようになります。
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" ディレクティブがその関数をウィジェットとしてマークします。onPress は部分的な状態更新を返します — これは現在の状態とマージされて再レンダリングされます。アプリを起動する必要はありません。
ウィジェットと Live Activity のユースケース
- ウィジェット: 1日のあいだに何度も確認するような情報向け(次のカレンダー予定、現在の天気、タスク数、習慣の継続日数 など)
- Live Activities: 終了の明確なある進行中イベント向け(配達の ETA カウントダウン、試合のスコアとゲームクロック、フライトステータス、ライドシェアのドライバー位置、ワークアウトタイマー など)
覚えやすいルール: ウィジェットは「チェックイン」向け、Live Activities は「進行中」向け。
今後の予定
今後追加を予定している主な機能:
- Timeline readback — アプリが閉じている間に適用されたタイムライン更新をアプリ側から取得できる仕組み(アプリロジックへ同期するため)
- Refresh policy — タイムラインをスケジュールする際、WidgetKit にいつ新しいタイムラインを要求するかを指定するポリシー(たとえば最後のエントリ表示後や特定日時など)
- Images — 現状の ExpoUI は画像表示をサポートしていません(画像表示のサポートを計画中)
expo-widgets は現在 alpha です。フィードバックを集めて粗さを改善していく中で、いくつかの API が変更される可能性があります。ぜひあなたが作ったものや意見を聞かせてください。動作しない点や機能要望があればお知らせください!