Rork でアプリを公開した後、「ホーム画面ウィジェットを付けたい」と思った瞬間に、初めて Expo というプラットフォームの境界線に触れることになります。チャットで「ウィジェットを追加して」と頼んでも、Rork の AI は React Native のコンポーネントを生成するだけで、ホーム画面に置けるウィジェットは出てきません。私も最初にこれを試したとき、生成されたのはアプリ内の「ウィジェット風カード UI」でした。要求した側の意図とはまったく別物です。
なぜそうなるのか、そしてどうすれば本物のウィジェットを Rork 製アプリに組み込めるのか。壁紙アプリで「日替わり画像をウィジェットに表示する」機能を検証したときの手順を整理します。2014年から個人開発を続けてきて、ネイティブの UIKit 側では何度もウィジェットを実装してきましたが、Expo ベースの Rork アプリに組み込むのは勝手が大きく違いました。
ウィジェットが「アプリの外」で動く拡張ターゲットだから生成できない
最初に押さえておきたいのは、iOS のホーム画面ウィジェットは React Native のコードでは書けない、という構造的な事実です。
WidgetKit のウィジェットは、アプリ本体とは別の「App Extension」という独立したバイナリとして動きます。実行されるのはアプリのプロセスではなく、システム側のプロセスです。そこで動かせるのは SwiftUI のビューだけで、JavaScript ランタイムは存在しません。つまり React Native / Expo のコードがどれだけ完成していても、ウィジェット部分だけは Swift で書く必要があります。
Rork のチャット AI が生成するのは Expo(React Native)のコードなので、「ウィジェットを作って」と頼んでもアプリ内 UI しか出てこないのは、AI の能力不足ではなく実行環境の制約です。ここを誤解したまま何度もプロンプトを書き直すと時間だけが溶けていきます。私は最初の 30 分をここで失いました。
実装ルートは 3 つ — 私が config plugin 方式を選んだ理由
Rork 製アプリにウィジェットを足す現実的なルートは、次の 3 つに絞られます。
- ① config plugin(
@bacons/apple-targets等)で拡張ターゲットを注入する: Expo の managed workflow を保ったまま、ビルド時にウィジェット用の Xcode ターゲットを自動生成する方式です - ② prebuild して bare workflow に降りる:
npx expo prebuildで ios フォルダを展開し、Xcode で直接ウィジェットターゲットを追加する方式です。自由度は最高ですが、以後 Rork のチャットでの修正と手元のネイティブ変更が衝突しやすくなります - ③ Rork Max の SwiftUI ネイティブ生成に乗り換える: アプリ全体を SwiftUI で生成し直す選択肢です。ウィジェットとの親和性は最も高いものの、既存の Expo アプリを作り直すコストが発生します
既に運用中の Expo アプリに後付けするなら、私は ① を選びます。理由は一つで、Rork 側のコード生成フローを壊さずに済むからです。② に降りると、その後チャットで UI を修正するたびにネイティブ側との整合性を自分で管理する必要が出てきます。個人開発で複数アプリを並行運用している身としては、この管理コストが一番重いと感じています。
ビルドは EAS Build(クラウド)に任せられるので、Mac の Xcode を開かずに完結できる点も ① の利点です。クラウドビルドの基本的な流れは Rork Max クラウドコンパイル — Mac不要でネイティブアプリを構築 で書いた内容がそのまま使えます。
App Group でアプリとウィジェットのデータを橋渡しする
ウィジェット実装で最初に設計すべきはデータの受け渡しです。アプリ本体(JS 側)とウィジェット(Swift 側)は別プロセスなので、変数もストレージも共有されません。両者をつなぐ標準的な手段が App Group です。
仕組みは単純で、group.com.example.myapp のような識別子を両ターゲットに設定すると、その名前空間の UserDefaults を双方から読み書きできるようになります。React Native 側からは expo-shared-group-preferences 系のモジュール、もしくは小さな Expo Module を自作して書き込みます。
検証した壁紙ウィジェットでは、JS 側が「今日の画像 URL とタイトル」を App Group に書き、Swift 側のウィジェットがそれを読んで表示する、という一方向の流れにしました。双方向同期にすると排他制御の考慮が増えるので、ウィジェットは「読み取り専用のビュー」と割り切るのが安全だと考えています。
// JS側: 日替わりコンテンツを App Group の UserDefaults に書き込む
// (expo-modules-core で自作した小さなネイティブモジュール経由)
import { setWidgetData } from "./modules/widget-bridge";
export async function publishTodayToWidget(item: WallpaperItem) {
// ウィジェットが読むキーは1つの JSON にまとめる(キー散らばり防止)
await setWidgetData("group.net.example.wallpaper", {
title: item.title, // 例: "雨上がりの新宿御苑"
imageUrl: item.thumbnailUrl, // ウィジェットは小サイズ画像で十分
updatedAt: new Date().toISOString(),
});
// 期待する動作: 次回のタイムライン更新時にウィジェットへ反映される
}ここで注意したいのは画像の扱いです。ウィジェットのプロセスにはメモリ上限(おおよそ 30MB 前後)があり、フルサイズの壁紙画像を読み込むと描画ごと失敗します。サムネイル URL を渡すか、事前に縮小した画像を App Group の共有コンテナにファイルとして置くのが現実的でした。
WidgetKit 側は「タイムライン」の考え方さえ掴めば短い
Swift 側のコードは、config plugin が生成したターゲットの中に置きます。WidgetKit は「今後表示すべきスナップショットの配列(タイムライン)」を OS に渡す設計で、ここさえ理解すれば書く量は多くありません。
// Widget側: App Group から日替わりデータを読んで表示する
struct DailyEntry: TimelineEntry {
let date: Date
let title: String
}
struct DailyProvider: TimelineProvider {
func getTimeline(in context: Context,
completion: @escaping (Timeline<DailyEntry>) -> Void) {
// アプリ本体が書き込んだ App Group の UserDefaults を読む
let store = UserDefaults(suiteName: "group.net.example.wallpaper")
let title = store?.string(forKey: "title") ?? "本日の一枚"
let entry = DailyEntry(date: Date(), title: title)
// 次の更新は翌朝6時に1回だけ依頼する(更新予算の節約)
let next = Calendar.current.nextDate(after: Date(),
matching: DateComponents(hour: 6), matchingPolicy: .nextTime)!
completion(Timeline(entries: [entry], policy: .after(next)))
}
// placeholder / getSnapshot は省略(実装は数行です)
}
// 期待する動作: ホーム画面のウィジェットに「本日の一枚」のタイトルが表示され、
// 毎朝6時のタイムライン更新で新しい内容に切り替わる公式ドキュメントを読むだけでは気づきにくいのが 更新頻度の「予算」 です。ウィジェットのタイムライン更新は OS がスケジュールし、1 日あたりの回数に上限があります(体感では 40〜70 回程度に制限されます)。「15 分ごとに更新」のようなタイムラインを組むと、予算切れで日中の後半はまったく更新されなくなります。日替わりコンテンツなら 1 日 1 回の .after 指定で十分で、即時反映が必要な操作だけアプリ側から WidgetCenter.shared.reloadAllTimelines() を呼ぶ構成が安定しました。
ビルドと実機確認で詰まった 2 点
実装より時間を取られたのがビルド周りでした。詰まった点を 2 つ残しておきます。
1 つ目は プロビジョニングです。ウィジェットは独立したターゲットなので、Bundle ID(例: net.example.wallpaper.widget)にも個別のプロビジョニングプロファイルが必要になります。EAS Build は基本的に自動生成してくれますが、App Group の entitlement を後から足した場合、古いプロファイルが残って署名エラーになることがありました。eas credentials から該当プロファイルを一度削除して再生成すると解消します。development / preview / production でプロファイルを切り替えている場合の注意点は Rork の EAS Build プロファイルを切り替えたら動かなくなった — development・preview・production の落とし穴と対処法 も参考になるはずです。
2 つ目は 実機確認の方法です。ウィジェットはシミュレーターでも動きますが、タイムライン更新の挙動(予算による間引き)はシミュレーターと実機でまったく違います。日次更新の検証は実機で 2〜3 日動かして初めて信頼できる結果になりました。実機への入れ方自体は Rork Companion で iPhone 実機テスト — Apple Developer登録不要でQRコード1枚から始める の手順が使えますが、ウィジェットを含むビルドは Companion のプレビューでは確認できず、EAS Build → TestFlight 経由になる点だけ注意してください。
まとめ — まず「読み取り専用の小さなウィジェット」から
ウィジェットは機能を盛るほどメモリ上限と更新予算に苦しむので、最初の 1 個は「App Group から文字列を 1 つ読んで表示するだけ」の最小構成で作ってみてください。そこまで通れば、画像表示やディープリンク対応は積み増しで対応できます。累計 5,000 万 DL 規模まで壁紙アプリ群を育ててきた経験からも、ウィジェットのような「毎日目に入る小さな接点」は起動率への効きが想像以上に大きい機能です。Expo の制約はありますが、越える価値は十分にあると感じています。
お読みいただきありがとうございました。