「ホーム画面ウィジェットを付けたい。これはもう Rork Max(月 $200)に移るしかないですよね?」——個人開発でアプリを長く運用している知人から、先月こう相談されました。結論から言うと、その時点では移る必要はありませんでした。ウィジェットは、標準 Rork が生成する Expo(React Native)構成のままでも届く側の機能だったからです。
Rork Max が打ち出すネイティブ機能の一覧——AR/LiDAR、Metal による 3D、ウィジェット、Dynamic Island、Live Activities、Siri Intents、HealthKit、HomeKit、NFC、App Clips、オンデバイス Core ML——を並べて眺めると、「React Native ではどれも無理そう」という印象を受けます。けれど実際に手を動かすと、その境界線は一覧が思わせるより手前にありません。多くは Expo のまま、あるいは小さな自作モジュールで届きます。問題は「どれが本当に Max を必要とするか」を、課金する前に見極められるかどうかです。
一覧を「機能の有無」で読むと判断を誤る
製品ページの機能一覧は「Max ならこれができる」を伝えるためのもので、「React Native ではこれができない」を意味しているわけではありません。ここを取り違えると、Expo のままで 1 日で足りた機能のために月 $200 を払い続けることになります。
判断の単位を「機能が存在するか」から「自分のアプリで、その機能にどれだけの実装距離があるか」に変えると、見え方が一気に変わります。私自身、壁紙系・癒し系のアプリを Expo 寄りの構成で運用してきた経験から言うと、ネイティブ機能の 7 割くらいは「Expo の世界の中で解決できる」側に落ちます。残りの 2 割が「自作のネイティブモジュールを 1 つ書けば届く」、本当に Max が現実解になるのは最後の 1 割です。
そこで、Max が挙げる機能を実装距離で 3 つの階層に振り分けてみます。
機能を3階層に振り分ける
| 階層 | 意味 | このあたりの機能 | 判断 |
| Tier 1 |
Expo のまま、config plugin か保守されたライブラリで届く |
プッシュ通知、アプリ内課金、位置情報、カメラ/写真、NFC タグ読み取り、HealthKit の基本的な読み取り |
Max 不要。標準 Rork で完結 |
| Tier 2 |
Expo prebuild に拡張ターゲットや自作ネイティブモジュールを足せば届く(development build 必須) |
ホーム画面ウィジェット、Live Activities/Dynamic Island、App Clips、App Intents/Siri、オンデバイス Core ML、Apple Foundation Models |
工数次第。1 機能あたり数日で見積もれるなら Expo に留まる価値あり |
| Tier 3 |
Expo で実用水準に届きにくく、ネイティブ Swift 生成(Rork Max)が現実解 |
RoomPlan/LiDAR の本格スキャン、Metal による 3D ゲーム、Apple Watch/TV/Vision Pro への同時展開 |
要件に入った時点で Max を真剣に検討 |
冒頭のウィジェットの相談は Tier 2 でした。「移るしかない」ではなく「数日で足りるか、それとも Max に任せた方が楽か」を測る話に変わります。ここが、課金判断の出発点です。
課金前に「自分のアプリがどの階層か」を実機で確かめる
階層表は地図にすぎません。自分のアプリが地図のどこにいるかは、実機で確かめるのが一番速いです。私が使っている手順は 4 ステップです。
まず、欲しい機能の config plugin か保守されたライブラリがあるかを探します。npx expo install が通り、app.json のプラグイン配列に足すだけで動くなら、それは Tier 1 です。
# Tier 1 かどうかの最初の確認。これが通れば Expo のまま届く
npx expo install expo-notifications react-native-nfc-manager
# config plugin を app.json に追記したら、prebuild で iOS プロジェクトを生成
npx expo prebuild -p ios --clean
次に、ライブラリでは足りず拡張ターゲット(ウィジェット等)やネイティブモジュールが要るなら、EAS の development build を作って実機に載せます。Expo Go では自作ネイティブコードは動かないので、ここで development build に切り替えるのが要点です。
# 自作ネイティブを含む検証は Expo Go ではなく development build で
eas build --profile development --platform ios
# ローカルでビルドできる環境なら(Mac + Xcode)こちらでも可
npx expo run:ios --device
3 つめに、自作モジュールが必要な場合の工数を見積もります。Expo Modules API で 1 機能を包むのに、調べる時間を含めて何日かかりそうか。数日に収まりそうなら Tier 2 として Expo に留まる価値があります。見積もりが 2 週間を超え始めたら、その機能は実質 Tier 3 に近いと判断します。
最後に、それでも届かない、あるいは Apple の複数機種への同時展開が要件に入っているなら、そこではじめて Max が候補になります。順序が逆——先に Max を契約してから何を作るか考える——だと、月 $200 が「使っていない保険」になりがちです。
ウィジェット拡張を Expo prebuild で生成する(Tier 2 の最小実装)
Tier 2 の代表であるウィジェットを例に、Expo のまま拡張ターゲットを足す最小構成を見ます。ここでは Expo の config plugin で WidgetKit の拡張ターゲットを prebuild 時に生成し、中身の SwiftUI だけ自分で書く形にします。
まず app.json にプラグインと拡張ターゲットの場所を宣言します。
{
"expo": {
"name": "MyWallpaperApp",
"ios": { "bundleIdentifier": "net.dolice.mywallpaper" },
"plugins": [
[
"@bacons/apple-targets",
{ "appleTeamId": "YOUR_TEAM_ID" }
]
]
}
}
次に拡張ターゲット側の定義を置きます。targets/widget/expo-target.config.js にターゲット種別を書くと、prebuild がウィジェット拡張を Xcode プロジェクトに組み込んでくれます。
// targets/widget/expo-target.config.js
module.exports = {
type: "widget",
name: "MyWallpaperWidget",
// 本体アプリと App Group を共有して、選択中の壁紙 ID を渡す
entitlements: {
"com.apple.security.application-groups": ["group.net.dolice.mywallpaper"],
},
};
ウィジェットの中身は SwiftUI で書きます。React Native 側からは App Group の UserDefaults を介して状態を渡すのが、いちばん壊れにくい受け渡し方です。
// targets/widget/MyWallpaperWidget.swift
import WidgetKit
import SwiftUI
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> Entry { Entry(date: .now, title: "—") }
func getSnapshot(in context: Context, completion: @escaping (Entry) -> Void) {
completion(loadEntry())
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
// 1 時間ごとに本体アプリが書いた最新の壁紙名を読み直す
let next = Calendar.current.date(byAdding: .hour, value: 1, to: .now)!
completion(Timeline(entries: [loadEntry()], policy: .after(next)))
}
private func loadEntry() -> Entry {
let defaults = UserDefaults(suiteName: "group.net.dolice.mywallpaper")
let title = defaults?.string(forKey: "currentWallpaper") ?? "未選択"
return Entry(date: .now, title: title)
}
}
struct Entry: TimelineEntry { let date: Date; let title: String }
struct MyWallpaperWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "MyWallpaperWidget", provider: Provider()) { entry in
VStack(alignment: .leading) {
Text("今日の壁紙").font(.caption).foregroundStyle(.secondary)
Text(entry.title).font(.headline)
}
.padding()
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("壁紙ウィジェット")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
React Native 側からは、壁紙を切り替えたときに App Group へ書き込み、reloadAllTimelines 相当を呼ぶ薄い橋渡しを 1 つ書くだけです。この粒度なら、調べる時間を入れても数日で実機まで届きます。これが「Tier 2 だが Expo に留まる価値がある」の具体的な手応えです。
つまずきやすい3つの落とし穴
ここで実際に時間を溶かしやすい箇所を、先に挙げておきます。
第一に、App Group の識別子を本体とウィジェットで一字でも違えると、データは黙って nil になります。ビルドは通り、ウィジェットも表示されるのに中身だけ空、という形で出るので原因に気づきにくいところです。group. 接頭辞ごと定数にして両側から参照するのが安全です。
第二に、Expo Go で動かそうとして「動かない」と誤判定することです。自作ネイティブを含む拡張は Expo Go では決して動きません。development build を作らずに「Expo では無理だ」と結論を出すと、Tier 2 を Tier 3 と読み違えて不要な課金に進んでしまいます。
第三に、appleTeamId の指定漏れです。App Group や拡張ターゲットは署名に Team ID が要るため、ここが空だと prebuild は通っても実機インストールで弾かれます。EAS の認証情報と app.json の Team ID を揃えておきます。
片方に振り切らず、両刀で運用するという手
3 階層に分けると、アプリ全体をどちらかに寄せきる必要がない、という選択肢も見えてきます。Dolice Labs で運用している複数アプリでも、機能ごとに「これは Expo の自作モジュールで」「これは将来 Max に出す」と振り分ける前提で設計しています。たとえば本体の体験は Expo に置いたまま、ウィジェットだけを Tier 2 として小さく足し、もし後から LiDAR スキャンのような Tier 3 が要件に入ったら、その機能だけを Max 側の検証に回す——という分け方です。
この見方の利点は、課金判断を「アプリ全体の引っ越し」という重い決断から、「この 1 機能を、どの階層の道具で実装するか」という軽い積み重ねに変えられることです。実際、私は新機能を 1 つ足すたびに、冒頭の階層表に照らして「今回はどこか」を一度書き出すようにしています。1 分の手間ですが、印象だけで $200 に手が伸びるのを確実に止めてくれます。
それでも Max を選ぶ、はっきりした2つの条件
3 階層で見極めたうえで、私が「ここまで来たら Max」と考える条件は 2 つです。
1 つは、Tier 3 の機能が要件の中心にあるときです。LiDAR で部屋を本格スキャンする、Metal で 3D を回す、といった機能は、Expo に橋を架けても保守が重くなり続けます。ネイティブ Swift を直接生成してくれる Max の土俵で戦った方が、長期の運用コストは下がります。
もう 1 つは、iPhone だけでなく Apple Watch・TV・Vision Pro へ同時に展開する事業計画があるときです。複数機種への展開は、React Native の上に積み増すより、はじめからネイティブ生成に乗せた方が破綻しにくいと感じます。逆に言えば、この 2 条件のどちらにも当てはまらないなら、Tier 1・Tier 2 を Expo で埋めていく方が、個人開発の身の丈には合っています。
判断に迷ったら、まず欲しい機能を 1 つだけ取り出して、上の 4 ステップを実機まで回してみてください。自分のアプリが地図のどこにいるかが分かれば、月 $200 を払う・払わないは、印象ではなく手応えで決められるようになります。同じ岐路に立っている方の判断材料になれば嬉しいです。