React NativeでiOSとAndroid向けのカスタムウィジェットを作成
単一のReact Nativeアプリケーションから、iOSとAndroidの両方に対してネイティブホーム画面ウィジェットを構築する方法を学びます。このチュートリアルでは、ユーザーのホーム画面に毎日の歩数や睡眠スコアなどの主要な健康統計を共有し表示する方法を説明します。
このページの内容
情報があふれる現代において、ホーム画面ウィジェットは、ユーザーがアプリを開かなくてもアプリからのデータを一目で確認できる強力な手段を提供します。健康・フィットネスアプリにとって、これはまさにゲームチェンジャーです。ユーザーがスマートフォンのロックを解除するたびに、日々の歩数や睡眠スコアが表示される姿を想像してみてください——常に、微細なモチベーションの源となるでしょう。
このチュートリアルでは、React Nativeアプリケーション内でiOSとAndroidの両方にカスタムホーム画面ウィジェットを構築するプロセスを解説します。ユーザーの一日あたりの歩数と睡眠スコアを表示する「Health Stats」ウィジェットを作成します。React Nativeには組み込みの「ウィジェット」コンポーネントありませんが、ネイティブモジュールを活用することでこれらの強力な拡張機能を作成できます。
前提条件:
- React Nativeの基本的な理解
- Node.jsとnpm/yarnがインストール済み
- iOSとAndroidの両方でReact Native開発環境がセットアップ済み
- iOS開発用:XcodeがインストールされたMac
- Android開発用:Android Studioがインストール済み
- iOSでのApp Group機能にはアクティブなApple Developerアカウントが必要です
開発者にとってなぜ重要か:
ウィジェットは、ユーザーエンゲージメントとアプリのリテンションを大幅に向上させることができます。このチュートリアルを終える頃には、React Nativeアプリのカスタムウィジェットを構築するスキルを習得し、より統合された価値あるユーザー体験を提供できるようになるでしょう。
問題の理解
React Nativeでウィジェットを作成する際の主な課題は、ウィジェットは本質的にネイティブUIコンポーネントであるということです。ウィジェットはメインアプリケーションとは別のプロセスで実行され、JavaScriptコンポーネントを直接レンダリングできません。そのため、ウィジェットのUIはプラットフォームのネイティブツール(iOSはSwiftUI、AndroidはXMLレイアウト)を使用して構築し、React Nativeアプリからこれらのネイティブウィジェットにデータを共有する信頼できる方法を確立する必要があります。
アプローチは以下の通りです:
- 各プラットフォームでネイティブにウィジェットUIを作成する
- React Nativeアプリとネイティブウィジェットの両方がアクセスできる共有データ層を確立する
- React Nativeアプリからこの共有ストレージにデータを書き込む
- ネイティブウィジェットでデータを読み取って表示する
前提条件
始める前に、React Nativeプロジェクトがセットアップされていることを確認してください。ない場合は、以下のコマンドで作成できます:
npx react-native init HealthStatsWidgetApp
cd HealthStatsWidgetApp
React Nativeアプリとネイティブウィジェットの間でデータの共有を容易にするライブラリも必要です。これにはiOS用にはreact-native-shared-group-preferencesを、Android用にはカスタムネイティブモジュールを使用します。
ステップ1:iOSウィジェットの構築
何をするか
iOSでは、Xcodeで「Widget Extension」を作成し、SwiftUIを使用してウィジェットのUIをデザインします。次に、メインアプリとウィジェット拡張機能の間でデータ共有を有効にする「App Group」を設定します。
実装
1. XcodeでWidget Extensionを作成する:
- Xcodeでプロジェクトの
iosフォルダを開く(open ios/HealthStatsWidgetApp.xcworkspace)。 File > New > Target...に移動する。- 「Widget Extension」を選択し、「Next」をクリックする。
- ウィジェットの名前を入力(例如:
HealthStatsWidget)。このシンプルな例では「Include Configuration Intent」のチェックを外す。 - 「Finish」をクリックする。プロンプトが表示されたら新しいスキームをアクティブにする。
2. App Groupsを設定する:
- Xcodeでメインアプリターゲット(
HealthStatsWidgetApp)を選択する。 - 「Signing & Capabilities」タブに移動し、「+ Capability」をクリックして「App Groups」を追加する。
- 「App Groups」の下にある「+」ボタンをクリックし、
group.com.yourcompany.yourappnameという形式の痛み型識別子を持つ新しいグループを追加する。 - ウィジェット拡張機能ターゲット(
HealthStatsWidget)を選択し、同じプロセスを繰り返してまったく同じApp Groupを追加する。
3. React NativeからiOSウィジェットにデータを共有する:
まず、共有プレファレンス用のパッケージをインストールします:
npm install react-native-shared-group-preferences
cd ios && pod install
次に、React Nativeコード(例:App.js)で共有ストレージにデータを書き込むことができます:
// src/App.js
import React from 'react';
import { SafeAreaView, Button, View, Text } from 'react-native';
import SharedGroupPreferences from 'react-native-shared-group-preferences';
const App = () => {
const appGroupIdentifier = 'group.com.yourcompany.HealthStatsWidgetApp'; // App Group識別子に置き換えてください
const updateWidgetData = async () => {
const healthData = {
steps: Math.floor(Math.random() * 10000),
sleepScore: Math.floor(Math.random() * 100),
};
try {
await SharedGroupPreferences.setItem('healthData', healthData, appGroupIdentifier);
console.log('共有ストレージにデータを保存しました');
} catch (error) {
console.error('データ保存エラー:', error);
}
};
return (
<SafeAreaView>
<View style={{ padding: 20 }}>
<Text style={{ fontSize: 18, marginBottom: 20 }}>Health Stats App</Text>
<Button title="Update Widget Data" onPress={updateWidgetData} />
</View>
</SafeAreaView>
);
};
export default App;
4. SwiftUIウィジェットでデータを表示する:
次に、ウィジェットのSwiftコードを修正してデータを読み取り、表示します。XcodeでHealthStatsWidget.swiftファイルを開きます。
// HealthStatsWidget/HealthStatsWidget.swift
import WidgetKit
import SwiftUI
struct HealthData: Codable {
let steps: Int
let sleepScore: Int
}
struct Provider: TimelineProvider {
let appGroupIdentifier = "group.com.yourcompany.HealthStatsWidgetApp" // App Group識別子に置き換えてください
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), healthData: HealthData(steps: 8000, sleepScore: 85))
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), healthData: HealthData(steps: 8000, sleepScore: 85))
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let userDefaults = UserDefaults(suiteName: appGroupIdentifier)
var entries: [SimpleEntry] = []
if let savedData = userDefaults?.object(forKey: "healthData") as? [String: Any] {
let steps = savedData["steps"] as? Int ?? 0
let sleepScore = savedData["sleepScore"] as? Int ?? 0
let entry = SimpleEntry(date: Date(), healthData: HealthData(steps: steps, sleepScore: sleepScore))
entries.append(entry)
} else {
let entry = SimpleEntry(date: Date(), healthData: HealthData(steps: 0, sleepScore: 0))
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let healthData: HealthData
}
struct HealthStatsWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
Text("Health Stats")
.font(.headline)
Text("Steps: \(entry.healthData.steps)")
Text("Sleep Score: \(entry.healthData.sleepScore)")
}
}
}
@main
struct HealthStatsWidget: Widget {
let kind: String = "HealthStatsWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
HealthStatsWidgetEntryView(entry: entry)
}
.configurationDisplayName("Health Stats")
.description("Your daily health stats.")
}
}
仕組み
- App Groupsはデバイス上に安全で共有されたコンテナを作成し、メインアプリとその拡張機能の両方がアクセスできます。
react-native-shared-group-preferencesは、ネイティブのUserDefaults(iOS用)へのブリッジを提供し、JavaScriptからこの共有コンテナにデータを書き込むことができます。- Swiftコードの
TimelineProviderは、データを取得し、ウィジェットの更新タイミングを決定する役割を担います。 getTimeline関数は、同じApp Group識別子を使用してUserDefaultsからデータを読み取り、ウィジェットのSwiftUIビューで表示するタイムラインエントリを作成します。
一般的な落とし穴
- App Group識別子がメインアプリターゲット、ウィジェット拡張機能ターゲット、およびReact Nativeコードで同一であることを確認してください。
- App Groupsを使用するには、アクティブなApple Developerアカウントが必要です。
- ウィジェットの更新は即時ではなく、バッテリー節約のためにOSによって管理されます。
ステップ2:Androidウィジェットの構築
何をするか
Androidでは、Android Studioを使用してAppWidgetProviderとXMLレイアウトのウィジェットを作成します。データ共有には、SharedPreferencesを活用するカスタムネイティブモジュールを作成します。
実装
1. Android Studioでウィジェットを作成する:
- プロジェクトの
androidフォルダをAndroid Studioで開く。 appフォルダを右クリックし、New > Widget > App Widgetに移動する。Class NameをHealthStatsWidgetProviderに設定し、Layout Nameをhealth_stats_widgetに設定する。「Finish」をクリックする。
2. ウィジェトレイアウトをデザインする:
Android Studioはres/layoutディレクトリにhealth_stats_widget.xmlを作成します。開いて以下を追加します:
<!-- res/layout/health_stats_widget.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:background="@android:color/white"
android:padding="16dp">
<TextView
android:id="@+id/widget_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Health Stats"
android:textColor="@android:color/black"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/widget_steps"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Steps: 0"
android:textColor="@android:color/black" />
<TextView
android:id="@+id/widget_sleep"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Sleep Score: 0"
android:textColor="@android:color/black" />
</LinearLayout>
3. データ共有用のネイティブモジュールを作成する:
React NativeアプリがネイティブAndroid側と通信できるようにブリッジを作成する必要があります。
まず、新しいJavaクラスSharedStorageModule.javaを作成します:
// android/app/src/main/java/com/healthstatswidgetapp/SharedStorageModule.java
package com.healthstatswidgetapp;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import android.content.Context;
import android.content.SharedPreferences;
public class SharedStorageModule extends ReactContextBaseJavaModule {
ReactApplicationContext context;
public SharedStorageModule(ReactApplicationContext reactContext) {
super(reactContext);
context = reactContext;
}
@Override
public String getName() {
return "SharedStorage";
}
@ReactMethod
public void set(String data) {
SharedPreferences.Editor editor = context.getSharedPreferences("DATA", Context.MODE_PRIVATE).edit();
editor.putString("healthData", data);
editor.apply();
}
}
次に、このモジュールを登録するパッケージSharedStoragePackage.javaを作成します:
// android/app/src/main/java/com/healthstatswidgetapp/SharedStoragePackage.java
package com.healthstatswidgetapp;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SharedStoragePackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new SharedStorageModule(reactContext));
return modules;
}
}
最後に、MainApplication.javaでパッケージを登録します:
// android/app/src/main/java/com/healthstatswidgetapp/MainApplication.java
// ...
import com.healthstatswidgetapp.SharedStoragePackage; // このインポートを追加
public class MainApplication extends Application implements ReactApplication {
// ...
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
// オートリンクされていないパッケージはここに手動で追加できます,例如:
packages.add(new SharedStoragePackage()); // この行を追加
return packages;
}
// ...
}
4. ウィジェットプロバイダーを更新してデータを表示する:
HealthStatsWidgetProvider.javaを修正して、SharedPreferencesからデータを読み取り、ウィジェットのビューを更新します。
// android/app/src/main/java/com/healthstatswidgetapp/HealthStatsWidgetProvider.java
package com.healthstatswidgetapp;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.SharedPreferences;
import android.widget.RemoteViews;
import org.json.JSONObject;
public class HealthStatsWidgetProvider extends AppWidgetProvider {
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
try {
SharedPreferences sharedPref = context.getSharedPreferences("DATA", Context.MODE_PRIVATE);
String healthDataString = sharedPref.getString("healthData", "{\"steps\":0,\"sleepScore\":0}");
JSONObject healthData = new JSONObject(healthDataString);
// RemoteViewsオブジェクトを構築
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.health_stats_widget);
views.setTextViewText(R.id.widget_steps, "Steps: " + healthData.getInt("steps"));
views.setTextViewText(R.id.widget_sleep, "Sleep Score: " + healthData.getInt("sleepScore"));
// ウィジェットマネージャーに対してウィジェットの更新を指示
appWidgetManager.updateAppWidget(appWidgetId, views);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// 複数のウィジェットがアクティブになっている場合があるため、すべてを更新
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
}
5. React NativeからAndroidウィジェットにデータを送信する:
次に、Android用の新しいネイティブモジュールを使用するようにReact Nativeコードを更新します。
// src/App.js(Android追加版)
import React from 'react';
import { SafeAreaView, Button, View, Text, NativeModules, Platform } from 'react-native';
import SharedGroupPreferences from 'react-native-shared-group-preferences';
const { SharedStorage } = NativeModules;
const App = () => {
const appGroupIdentifier = 'group.com.yourcompany.HealthStatsWidgetApp'; // iOS用
const updateWidgetData = async () => {
const healthData = {
steps: Math.floor(Math.random() * 10000),
sleepScore: Math.floor(Math.random() * 100),
};
if (Platform.OS === 'ios') {
try {
await SharedGroupPreferences.setItem('healthData', healthData, appGroupIdentifier);
console.log('iOSデータを共有ストレージに保存しました');
} catch (error) {
console.error('iOSデータ保存エラー:', error);
}
} else if (Platform.OS === 'android') {
try {
await SharedStorage.set(JSON.stringify(healthData));
console.log('Androidデータを共有ストレージに保存しました');
} catch (error) {
console.error('Androidデータ保存エラー:', error);
}
}
};
return (
<SafeAreaView>
<View style={{ padding: 20 }}>
<Text style={{ fontSize: 18, marginBottom: 20 }}>Health Stats App</Text>
<Button title="Update Widget Data" onPress={updateWidgetData} />
</View>
</SafeAreaView>
);
};
export default App;
仕組み
- カスタムネイティブモジュール
SharedStorageを作成し、setメソッドをJavaScriptコードに公開しました。 - このメソッドはAndroidの
SharedPreferencesを使用して、健康データをJSON文字列として保存します。 HealthStatsWidgetProviderはAppWidgetProviderであり、ウィジェットの更新を処理するブロードキャストレシーバーです。onUpdateメソッドでSharedPreferencesからデータを読み取り、JSONを解析し、RemoteViewsを使用してウィジェットのレイアウト内のテキストを更新します。
すべてをまとめる
iOSとAndroidの両方で手順を実装した後、両方のプラットフォームでアプリを実行できます。
テスト手順:
- シミュレーターまたはデバイスでアプリを実行する:
- iOSの場合:
npx react-native run-ios - Androidの場合:
npx react-native run-android
- iOSの場合:
- アプリを開き、「Update Widget Data」ボタンをタップする。
- ホーム画面に移動し、「Health Stats」ウィジェットを追加する。
- ウィジェットに健康データが表示されるはずです。
- アプリに戻り、もう一度ボタンをタップすると、ウィジェットが更新されます(注意:Androidでのウィジェットの更新は少し遅延する場合もあります)。
セキュリティのベストプラクティス
- 機密データ:この例では単純で機密性のないデータを使用しています。機密情報を表示する必要がある場合は、共有プレファレンスに保存する前にデータを暗号化することを検討してください。
- データのバリデーション:不正なデータによるクラッシュを防ぐため、ウィジェットに表示する前にネイティブ側で常にデータをバリデーションしてください。
結論
React Nativeアプリケーション用のクロスプラットフォームホーム画面ウィジェットを正常に構築しました!ネイティブコードに触れる必要がありますが、プロセスは管理可能であり、ユーザーのエンゲージメントを維持する強力な方法を開拓します。iOSとAndroidの両方でウィジェットのネイティブUIを作成する方法と、React Nativeアプリから情報を共有するためのデータブリッジを確立する方法を説明しました。
読者向け次のステップ:
- さまざまなウィジェットサイズとレイアウトを探求する。
- ユーザー操作なしでウィジェットを定期的に更新するためにバックグラウンドフェッチを実装する。
- ウィジェットのタップ時にアプリの特定の画面を開くディープリンクを追加する。
リソース
- Apple Developer: Widget Extensionの作成
- Android Developer: App Widgets
- react-native-shared-group-preferences
クイックリファレンス
| 項目 | 重要な情報 |
|---|---|
| テーマ | 開発 |
| 関連性 | 患者教育と気づき |
| アクション | 医療提供者にご相談ください |
| 頻度 | ケアチームのおすすめに従ってください |
| エビデンス | 現在の臨床ガイドラインに基づく |
よくある質問
Q: これが自分に当てはまるかどうかはどのようにわかりますか? A: 個人および家族の病歴を医療提供者と相談し、あなたのリスク因子を評価し、適切な次のステップを決定してください。
Q: 医師に何と聞けばよいですか? A: 良い質問例:「結果はどういう意味ですか?」「オプションは何がありますか?」「どんなライフスタイルの改善が役立ちますか?」「いつフォローアップすべきですか?」
Q: 生活を改善する方法はありますか? A: ほとんどの健康状態は基本的な措置から恩恵を受けます:バランスの取れた栄養、適切な運動(週150分以上)、質の良い睡眠(7〜9時間)、ストレス管理、および禁煙です。
医療に関する免責事項
この記事は教育目的のみを目的としており、医学的アドバイスを提供しません。健康、治療、またはケアに関する決定を行う前には、常に資格のある医療提供者に相談してください。個人の状況は異なり、1人に適切なことが別の人に適しているとは限りません。
緊急の医療問題がある場合は、直ちに119番(または現地の緊急電話番号)に連絡してください。