メインコンテンツへスキップ

ネイティブとReact Native間の通信

既存アプリとの統合ガイドネイティブUIコンポーネントガイドでは、React Nativeをネイティブコンポーネントに埋め込む方法、またその逆の方法を学びます。ネイティブとReact Nativeのコンポーネントを混在させると、最終的にこれら2つの世界の間で通信する必要性が出てきます。それを達成するためのいくつかの方法は、すでに他のガイドで言及されています。この記事では、利用可能なテクニックをまとめています。

はじめに

React NativeはReactにインスパイアされているため、情報フローの基本的な考え方は似ています。Reactのフローは一方向です。各コンポーネントが親とその内部状態にのみ依存するコンポーネントの階層を維持します。これはプロパティを使って行います。データは親から子へトップダウン方式で渡されます。祖先コンポーネントが子孫の状態に依存する場合、子孫が祖先を更新するために使用するコールバックを渡すべきです。

同じコンセプトがReact Nativeにも適用されます。フレームワーク内で純粋にアプリケーションを構築している限り、プロパティとコールバックでアプリを駆動できます。しかし、React Nativeとネイティブコンポーネントを混在させる場合、それらの間で情報を渡すための特定の、言語をまたぐメカニズムが必要になります。

プロパティ

プロパティは、コンポーネント間の通信で最も簡単な方法です。そのため、ネイティブからReact Nativeへ、そしてReact Nativeからネイティブへプロパティを渡す方法が必要です。

ネイティブからReact Nativeへのプロパティ渡し

React Nativeビューをネイティブコンポーネントに埋め込むためには、RCTRootViewを使用します。RCTRootViewはReact Nativeアプリを保持するUIViewです。また、ネイティブ側とホストされているアプリ間のインターフェースも提供します。

RCTRootViewには、任意のプロパティをReact Nativeアプリに渡すことができるイニシャライザがあります。initialPropertiesパラメータはNSDictionaryのインスタンスでなければなりません。この辞書は内部的にJSONオブジェクトに変換され、トップレベルのJSコンポーネントが参照できます。

objectivec
NSArray *imageList = @[@"https://dummyimage.com/600x400/ffffff/000000.png",
@"https://dummyimage.com/600x400/000000/ffffff.png"];

NSDictionary *props = @{@"images" : imageList};

RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"ImageBrowserApp"
initialProperties:props];
tsx
import React from 'react';
import {View, Image} from 'react-native';

export default class ImageBrowserApp extends React.Component {
renderImage(imgURI) {
return <Image source={{uri: imgURI}} />;
}
render() {
return <View>{this.props.images.map(this.renderImage)}</View>;
}
}

RCTRootViewは、読み書き可能なプロパティappPropertiesも提供します。appPropertiesが設定されると、React Nativeアプリは新しいプロパティで再レンダリングされます。更新は、新しいプロパティが以前のものと異なる場合にのみ実行されます。

objectivec
NSArray *imageList = @[@"https://dummyimage.com/600x400/ff0000/000000.png",
@"https://dummyimage.com/600x400/ffffff/ff0000.png"];

rootView.appProperties = @{@"images" : imageList};

いつでもプロパティを更新しても問題ありません。ただし、更新はメインスレッドで実行する必要があります。ゲッターはどのスレッドからでも使用できます。

注記

現在、ブリッジの起動中にappPropertiesを設定すると、変更が失われる可能性があるという既知の問題があります。詳細については、https://github.com/facebook/react-native/issues/20115 を参照してください。

一度にいくつかのプロパティだけを更新する方法はありません。代わりに独自のラッパーを構築することをお勧めします。

React Nativeからネイティブへのプロパティ渡し

ネイティブコンポーネントのプロパティを公開する問題については、この記事で詳しく説明されています。要するに、カスタムネイティブコンポーネントでRCT_CUSTOM_VIEW_PROPERTYマクロを使ってプロパティをエクスポートし、React Nativeではそのコンポーネントが通常のReact Nativeコンポーネントであるかのように使用します。

プロパティの限界

言語をまたぐプロパティの主な欠点は、ボトムアップのデータバインディングを可能にするコールバックをサポートしていないことです。JSのアクションの結果としてネイティブの親ビューから削除したい小さなRNビューがあると想像してみてください。情報はボトムアップで伝わる必要があるため、プロップスでそれを実現する方法はありません。

言語をまたぐコールバックの機能(こちらで説明)はありますが、これらのコールバックは必ずしも私たちが必要とするものではありません。主な問題は、それらがプロパティとして渡されることを意図していないことです。むしろ、このメカニズムはJSからネイティブのアクションをトリガーし、そのアクションの結果をJSで処理することを可能にします。

言語をまたぐ他のインタラクション方法(イベントとネイティブモジュール)

前の章で述べたように、プロパティの使用にはいくつかの制限があります。時にはプロパティだけではアプリのロジックを駆動するのに十分ではなく、より柔軟性のある解決策が必要になります。この章では、React Nativeで利用可能な他の通信技術について説明します。これらは、内部通信(RNのJS層とネイティブ層の間)だけでなく、外部通信(RNとアプリの「純粋なネイティブ」部分の間)にも使用できます。

React Nativeでは、言語をまたぐ関数呼び出しを実行できます。JSからカスタムネイティブコードを実行したり、その逆も可能です。残念ながら、作業している側に応じて、同じ目標を異なる方法で達成します。ネイティブ側では、イベントメカニズムを使用してJSのハンドラ関数の実行をスケジュールし、React Native側では、ネイティブモジュールによってエクスポートされたメソッドを直接呼び出します。

ネイティブからReact Nativeの関数を呼び出す(イベント)

イベントについてはこの記事で詳しく説明されています。イベントを使用しても実行時間の保証はないことに注意してください。イベントは別のスレッドで処理されるためです。

イベントは、React Nativeコンポーネントへの参照を必要とせずに変更できるため、強力です。しかし、それらを使用する際に陥りやすい落とし穴がいくつかあります。

  • イベントはどこからでも送信できるため、プロジェクトにスパゲッティスタイルの依存関係をもたらす可能性があります。
  • イベントは名前空間を共有するため、名前の衝突が発生する可能性があります。衝突は静的に検出されないため、デバッグが困難になります。
  • 同じReact Nativeコンポーネントの複数のインスタンスを使用し、イベントの観点からそれらを区別したい場合、おそらく識別子を導入し、イベントと共に渡す必要があります(ネイティブビューの`reactTag`を識別子として使用できます)。

ネイティブをReact Nativeに埋め込む際の一般的なパターンは、ネイティブコンポーネントのRCTViewManagerをビューのデリゲートにし、ブリッジ経由でJavaScriptにイベントを送り返すことです。これにより、関連するイベント呼び出しが一箇所にまとまります。

React Nativeからネイティブ関数を呼び出す(ネイティブモジュール)

ネイティブモジュールは、JSで利用可能なObjective-Cクラスです。通常、各モジュールのインスタンスがJSブリッジごとに1つ作成されます。これらは任意の関数や定数をReact Nativeにエクスポートできます。これらはこの記事で詳しく説明されています。

ネイティブモジュールがシングルトンであるという事実は、埋め込みの文脈でこのメカニズムを制限します。ネイティブビューに埋め込まれたReact Nativeコンポーネントがあり、ネイティブの親ビューを更新したいとします。ネイティブモジュールのメカニズムを使用すると、期待される引数だけでなく、親ネイティブビューの識別子も受け取る関数をエクスポートすることになります。その識別子は、更新する親ビューへの参照を取得するために使用されます。つまり、モジュール内で識別子からネイティブビューへのマッピングを保持する必要があります。

この解決策は複雑ですが、すべてのReact Nativeビューを管理する内部React Nativeクラスである`RCTUIManager`で使用されています。

ネイティブモジュールは、既存のネイティブライブラリをJSに公開するためにも使用できます。Geolocationライブラリは、このアイデアの実例です。

注意

すべてのネイティブモジュールは同じ名前空間を共有します。新しいものを作成する際には、名前の衝突に注意してください。

レイアウト計算フロー

ネイティブとReact Nativeを統合する際には、2つの異なるレイアウトシステムを統合する方法も必要です。このセクションでは、一般的なレイアウトの問題を取り上げ、それらに対処するためのメカニズムを簡単に説明します。

React Nativeに埋め込まれたネイティブコンポーネントのレイアウト

このケースはこの記事で説明されています。要約すると、すべてのネイティブReactビューは`UIView`のサブクラスであるため、ほとんどのスタイルとサイズの属性は期待通りに機能します。

ネイティブに埋め込まれたReact Nativeコンポーネントのレイアウト

固定サイズのReact Nativeコンテンツ

一般的なシナリオは、ネイティブ側でサイズがわかっている固定サイズのReact Nativeアプリがある場合です。特に、フルスクリーンのReact Nativeビューはこのケースに該当します。より小さなルートビューが必要な場合は、RCTRootViewのフレームを明示的に設定できます。

例えば、RNアプリを高さ200(論理)ピクセル、幅をホスティングビューの幅に合わせるには、次のようにします。

SomeViewController.m
- (void)viewDidLoad
{
[...]
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:appName
initialProperties:props];
rootView.frame = CGRectMake(0, 0, self.view.width, 200);
[self.view addSubview:rootView];
}

固定サイズのルートビューがある場合、JS側でその境界を尊重する必要があります。言い換えれば、React Nativeのコンテンツが固定サイズのルートビュー内に収まるようにする必要があります。これを確実にする最も簡単な方法は、Flexboxレイアウトを使用することです。絶対配置を使用し、Reactコンポーネントがルートビューの境界の外に表示される場合、ネイティブビューと重なり、一部の機能が予期せず動作する原因となります。例えば、「TouchableHighlight」はルートビューの境界の外でのタッチをハイライトしません。

ルートビューのサイズを動的に更新するために、そのframeプロパティを再設定することは全く問題ありません。React Nativeがコンテンツのレイアウトを処理します。

可変サイズのReact Nativeコンテンツ

場合によっては、当初サイズが不明なコンテンツをレンダリングしたいことがあります。サイズがJSで動的に定義されるとしましょう。この問題には2つの解決策があります。

  1. React Nativeビューを`ScrollView`コンポーネントでラップすることができます。これにより、コンテンツが常に利用可能であり、ネイティブビューと重ならないことが保証されます。
  2. React Nativeでは、JSでRNアプリのサイズを決定し、それをホスティングしている`RCTRootView`の所有者に提供することができます。所有者はその後、サブビューの再レイアウトとUIの一貫性を保つ責任を負います。これを`RCTRootView`の柔軟性モードで実現します。

RCTRootViewは4つの異なるサイズ柔軟性モードをサポートしています

RCTRootView.h
typedef NS_ENUM(NSInteger, RCTRootViewSizeFlexibility) {
RCTRootViewSizeFlexibilityNone = 0,
RCTRootViewSizeFlexibilityWidth,
RCTRootViewSizeFlexibilityHeight,
RCTRootViewSizeFlexibilityWidthAndHeight,
};

RCTRootViewSizeFlexibilityNoneがデフォルト値で、ルートビューのサイズを固定にします(ただし、`setFrame:`で更新することは可能です)。他の3つのモードでは、React Nativeコンテンツのサイズ更新を追跡できます。例えば、モードを`RCTRootViewSizeFlexibilityHeight`に設定すると、React Nativeはコンテンツの高さを測定し、その情報を`RCTRootView`のデリゲートに渡します。デリゲート内では、コンテンツが収まるようにルートビューのフレームを設定するなど、任意のアクションを実行できます。デリゲートは、コンテンツのサイズが変更されたときにのみ呼び出されます。

注意

JSとネイティブの両方で次元を柔軟にすると、未定義の動作につながります。例えば、ホスティングしている`RCTRootView`で`RCTRootViewSizeFlexibilityWidth`を使用している間、トップレベルのReactコンポーネントの幅を(`flexbox`で)柔軟にしないでください。

例を見てみましょう。

FlexibleSizeExampleView.m
- (instancetype)initWithFrame:(CGRect)frame
{
[...]

_rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"FlexibilityExampleApp"
initialProperties:@{}];

_rootView.delegate = self;
_rootView.sizeFlexibility = RCTRootViewSizeFlexibilityHeight;
_rootView.frame = CGRectMake(0, 0, self.frame.size.width, 0);
}

#pragma mark - RCTRootViewDelegate
- (void)rootViewDidChangeIntrinsicSize:(RCTRootView *)rootView
{
CGRect newFrame = rootView.frame;
newFrame.size = rootView.intrinsicContentSize;

rootView.frame = newFrame;
}

この例では、ルートビューを保持する`FlexibleSizeExampleView`ビューがあります。ルートビューを作成し、初期化してデリゲートを設定します。デリゲートはサイズの更新を処理します。次に、ルートビューのサイズ柔軟性を`RCTRootViewSizeFlexibilityHeight`に設定します。これは、React Nativeコンテンツの高さが変わるたびに`rootViewDidChangeIntrinsicSize:`メソッドが呼び出されることを意味します。最後に、ルートビューの幅と位置を設定します。ここで高さも設定していますが、高さをRN依存にしたため、効果はありません。

この例の完全なソースコードはこちらで確認できます。

ルートビューのサイズ柔軟性モードを動的に変更することは問題ありません。ルートビューの柔軟性モードを変更すると、レイアウトの再計算がスケジュールされ、コンテンツサイズがわかるとデリゲートの`rootViewDidChangeIntrinsicSize:`メソッドが呼び出されます。

注記

React Nativeのレイアウト計算は別のスレッドで行われますが、ネイティブUIビューの更新はメインスレッドで行われます。これにより、ネイティブとReact Nativeの間で一時的にUIの不整合が生じる可能性があります。これは既知の問題であり、私たちのチームは異なるソースからのUI更新を同期させる作業に取り組んでいます。

注記

React Nativeは、ルートビューが他のビューのサブビューになるまでレイアウト計算を行いません。React Nativeビューをその寸法がわかるまで非表示にしたい場合は、ルートビューをサブビューとして追加し、最初は非表示にします(`UIView`の`hidden`プロパティを使用)。その後、デリゲートメソッドでその可視性を変更します。