React Native (Expo CNG) + Swift で iOS Widget を開発する
概要
- React Native (Expo CNG: Managed Workflow) で
iosディレクトリ不要で Widget を実装できました - サンプルコードを alea12/react-native-expo-widget-example に置いています
- Simulator で動作検証する際は iPad Simulator を使いましょう
Expo CNG とは
React Native を使ってモバイルアプリ開発を始める場合、Expo を採用することが多いと思います。Expo の Continuous Native Generation (CNG) は、 ios や android といったディレクトリを作成せずにモバイルアプリの開発を進めるワークフローです。JavaScript/TypeScript によるアプリ本体の実装と package.json や app.json などを通じた設定の記述のみでモバイルアプリ開発が可能で、デフォルトではこの CNG が有効になっています[1]。
React Native で Widget を実装する手法
ディレクトリをシンプルに保ち、 JS/TS 実装 + 設定ファイル群を SSOT とすることができるのが CNG の強みですが、Swift や Kotlin などによる iOS/Android のネイティブコードがそのままでは実装できないという課題があります。これを解決するために提供されているのが Config plugins という機構で、プラグインを通じてネイティブコード依存の実装を追加できます。
現時点で JS/TS のみを使って Widget を開発する広く知られているワークフローはなく[2]、 Swift/Kotlin によるネイティブ実装が必要です。この記事では、オープンソースの Config plugin である bndkt/react-native-widget-extension を活用し、 ios ディレクトリを作成することなく iOS で動作する Widget を実装する方法を紹介します。
実装手順
以下に Step-by-step の実装手順を整理しましたが、最低限動作するコードを alea12/react-native-expo-widget-example に置いておきました。とりあえず動作させたい、という方はこちらをお試しください。
環境
Apple M2 Chip + macOS 13.5.1 (Ventura) で動作を確認しました。
ソフトウェア・ライブラリ
| 名前 | バージョン | 導入方法 |
|---|---|---|
| Node.js | 18.19.0 | nodenv |
| EAS CLI | 7.3.0 | npm install --global eas-cli |
| Xcode | Version 15.2 (15C500b) | App Store |
| fastlane | 2.219.0 | gem install fastlane |
| Expo | 50.0.7 | create-expo-app |
| React Native | 0.73.4 | create-expo-app |
Expo を使った React Native アプリの作成
npx create-expo-app expo-cng-widget-example -t expo-template-blank-typescript
cd expo-cng-widget-example
ライブラリの追加
npx expo install react-native-widget-extension
Widget 関連の設定
Swift による Widget 実装を格納するディレクトリを作成します。今回は widgets/ios というディレクトリを作成しました。
mkdir -p widgets/ios
作成したディレクトリのパスを app.json で以下のように指定します。
{
"expo": {
"plugins": [
[
"react-native-widget-extension",
{ "widgetsFolder": "widgets/ios" }
]
]
}
}
widgets/ios の実装
react-native-widget-extension を使うには、 widgetsFolder に以下のリソースが含まれる必要があります。それぞれ サンプルリポジトリ に配置しているので、必要に応じて参照してください。
Assets.xcassets-
Attributes.swift(空ファイルですが、このパスが存在しないとビルド時にエラーが発生します) Info.plistModule.swift- Widget 本体の実装 (本例では
Widget.swift,WidgetBundle.swiftとしました)
なお、今回は Widget を含むビルドを Simulator で確認できるところまでを対象範囲とするため、Widget の詳細な実装には踏み込みません。サンプルリポジトリに含んでいる .swift ファイルも Xcode で Widget を作成するときに自動で生成されるコードをそのまま使用しています。
EAS Build の設定
ビルドには EAS Build を使います。 EAS Build といえば React Native アプリをビルドするための hosted サービスのイメージが強いので誤解されがちですが、 eas-cli には --local オプションがあり、これを使うことでローカルで Simulator 向け / App Store 向けのビルドを作ることができます。このオプションを使用している限り費用はかからないので、手元の端末でビルドができる場合は場面に応じて活用したいところです。
EAS Build を使うには事前に Expo のアカウントが必要なので、持っていない場合は 事前に作成 しておきましょう。
eas login
eas build:configure
上記のコマンドで eas.json が生成されますが、今回はシミュレーターで動作を確認したいので以下のような設定を追加しておきます。
{
"build": {
"development": {
"ios": {
"simulator": true
}
}
}
}
Simulator 向けのビルド
以下のコマンドで、ローカルで Development Build を作成できます。ここまでに作成した程度の単純なアプリであれば、 MacBook Air (M2, 2022) で 5-10 分程度でビルドできます。
eas build --profile development --platform ios --local
Simulator へのアプリのインストール
ビルドに成功すると build-(unixtime).tar.gz というファイルができます。解凍すると拡張子が .app なファイルが出てきます。これを Simulator にインストールして動作を検証します。
iOS 17.2 時点で iPhone Simulator では Widget がインストールできない不具合があるため、Widget の動作検証には iPad Simulator を使ってください。Simulator が起動できたら、.app ファイルを Simulator の画面内にドラッグ & ドロップすることでアプリをインストールすることができます。
Widget の動作検証だけであれば Development Server (npm run start) は不要です。Simulator 内の任意のアプリアイコンを長押しし、 Edit Home Screen → 画面左上の + アイコンから Widget の追加画面を呼び出せば React Native アプリに紐づいた Widget が表示されます[4]。

アプリ本体と Widget 間の通信
アプリ本体と Widget 間でデータをやり取りしたい場合、よく使われるのが App Group です。特にアプリ本体と Widget とを共通の App Group とすることで使用できる UserDefaults という Key-Value Store を使い、アプリから Widget に情報を渡すのがよく見られる実装パターンです。
ただし、今回採用した bndkt/react-native-widget-extension では App Group に対応していません。GitHub 上で PR は作成されており、そのコメント内に App Group を実現するための手順が整理されているので必要な場合は試してみてください[5]。もしニーズがあればこちらも続編として記事にしてみます。
補足: サンプルリポジトリの構成
├── App.tsx // React Native アプリの Entrypoint
├── app.json // Expo の設定ファイル。今回は expo.plugins や extra.eas.build あたりの設定が肝
├── assets
├── babel.config.js
├── eas.json // EAS Build の設定ファイル
├── package-lock.json
├── package.json
├── tsconfig.json
└── widgets // Widget 向けのネイティブコード (今回は Swift) を格納するディレクトリ
└── ios
├── Assets.xcassets
├── Attributes.swift
├── Info.plist
├── Module.swift
├── Widget.swift
└── WidgetBundle.swift
まとめ
以上の手順で、React Native @ Expo CNG 環境で ios ディレクトリを作成することなく(かつて Managed Workflow と呼ばれていた環境で) iOS Widget を実装することができました。ここから Widget に実際の機能・デザインを実装していくことになりますが、僕の場合は Xcode で別途からのプロジェクトを立ち上げて、その中に ios/swift 内のコードを ⌘C → V して検証する、という方法をとっています。この辺りの作業手順はまだあまり正解を見つけていないので、もし上手いアプローチをご存じの方がいれば教えてください!
-
過去には CNG に概念的に近いワークフローが Managed Workflow と呼ばれていましたが、今は公式ドキュメントで
archive扱いとされています ↩︎ -
ご存じの方がいれば教えてください! ↩︎
-
事前に Ad-hoc Provisioning を使って検証用端末を登録する 必要があります ↩︎
-
なぜか iPhone Simulator ではこの一覧が空で、iOS 標準の Calendar.app などの Widget も表示されません...筆者はここで躓いて 1 日無駄にしました ↩︎
Discussion