iOSのウィジェットとライブアクティビティがExpo SDK 56で安定版に

SDK 56 で expo-widgets が安定版になった。React コンポーネントだけでホーム画面ウィジェットとロック画面のライブアクティビティを構築でき、SwiftUI は不要だ。

日本語
コピー
iOS widgets and Live Activities are stable in Expo SDK 56

Widget と Live Activity は、App を開くほうがむしろ遠回りになる場面でこそ力を発揮する。料理の配達予定時刻、ワークアウトのタイマー、フライトの状況、次の会議、保存したショートカット——こうした情報が欲しいとき、人はたいてい別のことに手を取られている。Widget と Live Activity があれば、ユーザーが App を開かなくても、その App は役に立ち続けられる。

以前、こうしたインターフェースを React Native App で作ろうとすると、Xcode target を別に立て、App Groups を設定し、SwiftUI のレイアウトコードを書き、その拡張を App の残りの部分と同期させながらずっと保守していく必要があった。

SDK 56 から、expo-widgets が安定版になったWidget と Live Activity を Expo UI で React コンポーネントとして書ける。Widget Extension target、App Group の設定、SwiftUI の足場はすべて Continuous Native Generation が面倒を見る。別の iOS プロジェクトを保守する必要はもうない。

X で見る

SDK 55 で alpha として出たライブラリが、磨き上げを経て正式版になった。考え方は変わっていない。Widget を React で書き、システムが SwiftUI 経由で描画し、更新は Expo App が管理する。SDK 56 がもたらしたのは安定性の約束と、実用的な改善の数々だ。Widget と Live Activity が自分の environment を直接受け取れるようになり、メイン App による事前レンダリングが不要になった。エラー処理も充実した。

Widget と Live Activity に取り組む価値

Widget と Live Activity を作る理由は UI の完成度ではなく、リーチにある。

App アイコンは、ユーザーがタップして初めて App に入れる。Widget はタップがゼロで、ユーザーが別のことをしている間に価値を届ける。Live Activity はイベントの進行中ずっと表示され、状態の変化に合わせて更新され、ロック画面でも消えない。ユーザーが App を丸ごと開く必要のないとき、これらのインターフェースは関連情報をすぐ近くに置き、App を補完する。

Flighty は良い公開事例だ。2023 年 Apple Design Awards のインタラクション部門を受賞しており、Apple の講評はここで最も重要な点を突いている。Live Activity、Dynamic Island との統合、Widget 対応、そして重要なフライト情報を旅客が最も必要とする場所に置くこと。フライト追跡 App にとって、これがプロダクト体験そのものだ。旅客が知りたいのはゲート変更、搭乗状況、到着時刻であって、App を何度も開くことではない。Flighty はその情報をロック画面と Dynamic Island に置き、旅が実際に進行している間に手の届くところへ届ける。

本番環境で実際に効くパターン:

  • Widgets は、ユーザーがすぐ手に取れる場所に欲しい情報(カレンダー、天気、習慣のチェック、残高)に使う。

  • Live Activities は、始まりと終わりがはっきりした「進行中」のイベント(配達予定、ワークアウトのタイマー、配車、飛行中のフライト)に使う。

どちらも、ユーザーが自分から開いていないときにも App の存在感を保つ。

OneSignal の 2024 年カスタマーエンゲージメント状況レポート(データはやや古いが、具体的ではある)によると、iOS の Live Activities を使う App は平均 30 日継続率が 23.7% 高かった。Live Activity を付ければ継続率が自動的に上がるという話ではないが、同じプロダクトの法則を指している。タイムリーな情報が適切な場所に現れれば、ユーザーは App に戻りやすくなる。

SDK 56 で何が安定したか

alpha 版は 3 月の SDK 55 とともに出た。その後、コミュニティでのテストと内部での反復が、安定版と言い切る前に削っておくべき粗さを炙り出した。主な変更点:

Widgets と Live Activities が WidgetEnvironment を通じて完全な環境情報を受け取るようになった。 widget family、コンテンツの余白、カラースキーム、レンダリングモード、そして表示やアクセシビリティに関するコンテキストが含まれ、システムがどこでどう widget を表示するかに合わせて調整できる。

Widgets と Live Activities は App 内での事前レンダリングが不要になった。 以前は起動時に App の React ツリーをたどって widget のレイアウトを用意する必要があったが、今は widget bundle が独立してレンダリングでき、その表示面に必要なコンテキストだけを受け取ればよい。

設定可能な widget。 SDK 56 は設定可能な widget に対応した。ユーザーは App 側で選択を済ませておく必要なく、widget ギャラリーで直接、widget に何を表示するかを選べる。

最初の widget を作る

最初の widget を作るための手順を以下に示す。動画のチュートリアルを見たい場合はこちら:

ライブラリをインストールして prebuild を実行すると、最小構成の widget はこうなる:

import { Button, Text, VStack } from '@expo/ui/swift-ui';
import { font, foregroundStyle } from '@expo/ui/swift-ui/modifiers';
import { createWidget, type WidgetEnvironment } from 'expo-widgets';

type Props = { count: number };

const CoffeeCounter = (props: Props, environment: WidgetEnvironment) => {
  'widget';

  return (
    <VStack spacing={8}>
      <Text modifiers={[font({ size: 48 })]}>☕</Text>
      <Text modifiers={[font({ size: 32, weight: 'bold' })]}>
        {props.count}
      </Text>
      <Button
        modifiers={[foregroundStyle('white')]}
        label="+"
        target="increment"
        onPress={() => ({ count: props.count + 1 })}
      />
    </VStack>
  );
};

export default createWidget('CoffeeCounter', CoffeeCounter);

'widget' ディレクティブは、その関数本体を widget のレンダリングコンテキストとしてマークする。これはメインアプリから隔離されたサンドボックスの JS ランタイムで動く。Expo UI コンポーネントは SwiftUI のプリミティブにそのまま対応しているため、widget はネイティブにレンダリングされ、widget のロジックは TypeScript 側に残る。

Live Activity も同じモデルに従う。違うのは、複数のスロット(ロック画面のバナー、Dynamic Island の compact / expanded / minimal)を記述し、システムが状況に応じて適切なものを選ぶ点だ。更新は APNs 経由でサーバーからプッシュすることも、アプリから直接トリガーすることもできる。

Widget と Live Activity、どちらを選ぶか

ここまでの概要はたいていのプロダクト議論には足りるが、実装上のトレードオフは両者で異なる。まず、ユーザーに何を見せたいのかをはっきりさせよう。

widget なら、アプリの外で常設の場所を占める価値のある情報は何か、どのくらいの頻度で変わる必要があるか、どのサイズが役に立つか、ユーザーがタップしたときに何が起きるべきかを決める。優れた widget はたいてい、はっきりしたことを一つだけやる。

Live Activity なら、まずライフサイクルを定義する。何がこの activity を開始するのか。アクティブな間にどんな変化が起こりうるか。終了時にどんな最終状態を残すべきか。更新はアプリから来るのか、サーバーからか、その両方か。これらが重要なのは、Live Activity にはプッシュトークン、サーバー側の更新ロジック、状態遷移、終了処理が絡むからだ。

どちらにするか迷ったら、たいていは widget から始めるほうがいい。工程が少なく、レンダリングモデルに慣れるのに実用的だ。この流れが手に馴染んだら、プロダクトの中に明確な開始・アクティブな更新・終了がある部分に Live Activity を足していこう。

expo-widgets から始める

expo-widgets をインストールし、widgets のドキュメント を参照して最初の widget を作ってみよう。

ユーザーがアプリの外で見られたら助かる情報を一つ選び、まずその widget を作る。

アプリに「進行中」の瞬間が自然に存在するなら、そのフローの Live Activity をプロトタイプしてみるといい。

開発中に問題が出たときや、次に何をすべきかアイデアがあるときは、DiscordGitHub で知らせてほしい。特に知りたいのは本番環境での導入事例と、共有してもいい保持率やエンゲージメントのデータだ。実際の数字は今後の優先順位に影響する。

出典: Expo Blog← ホームへ戻る