ネイティブと 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 コンポーネントが参照できます。
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];
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 アプリは新しいプロパティで再レンダリングされます。更新は、新しく更新されたプロパティが以前のプロパティと異なる場合にのみ実行されます。
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 にイベントを返送することです。これにより、関連するイベント呼び出しを 1 つの場所にまとめることができます。
React Native からネイティブ関数を呼び出す (ネイティブモジュール)
ネイティブモジュールは、JS で使用可能な Objective-C クラスです。通常、各モジュールの 1 つのインスタンスが JS ブリッジごとに作成されます。それらは、任意の関数と定数を 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(論理)ピクセルにし、ホストビューの幅を広くするには、次のようにします。
- (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'はルートビューの境界外でのタッチをハイライトしません。
フレームプロパティを再設定することで、ルートビューのサイズを動的に更新しても問題ありません。React Nativeはコンテンツのレイアウトを処理します。
可変サイズのReact Nativeコンテンツ
場合によっては、最初は不明なサイズのコンテンツをレンダリングしたい場合があります。サイズはJSで動的に定義されるとしましょう。この問題には2つの解決策があります。
- React Nativeビューを
ScrollView
コンポーネントでラップできます。これにより、コンテンツが常に利用可能になり、ネイティブビューと重なることがなくなります。 - React Nativeでは、JSでRNアプリのサイズを決定し、それをホスト側の
RCTRootView
のオーナーに提供できます。オーナーは、サブビューを再レイアウトし、UIの一貫性を維持する責任があります。これは、RCTRootView
の柔軟性モードで実現します。
RCTRootView
は、4つの異なるサイズ柔軟性モードをサポートしています。
typedef NS_ENUM(NSInteger, RCTRootViewSizeFlexibility) {
RCTRootViewSizeFlexibilityNone = 0,
RCTRootViewSizeFlexibilityWidth,
RCTRootViewSizeFlexibilityHeight,
RCTRootViewSizeFlexibilityWidthAndHeight,
};
RCTRootViewSizeFlexibilityNone
はデフォルト値で、ルートビューのサイズを固定にします(ただし、setFrame:
で更新することはできます)。他の3つのモードでは、React Nativeコンテンツのサイズ更新を追跡できます。たとえば、モードをRCTRootViewSizeFlexibilityHeight
に設定すると、React Nativeがコンテンツの高さを測定し、その情報をRCTRootView
のデリゲートに渡します。デリゲート内で、ルートビューのフレームを設定するなど、任意の操作を実行できるため、コンテンツが収まります。デリゲートは、コンテンツのサイズが変更された場合にのみ呼び出されます。
JSとネイティブの両方で寸法を可変にすると、未定義の動作が発生します。たとえば、ホスト側のRCTRootView
でRCTRootViewSizeFlexibilityWidth
を使用している間は、トップレベルのReactコンポーネントの幅を(flexbox
で)可変にしないでください。
例を見てみましょう。
- (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:
メソッドが1回呼び出されます。
React Nativeのレイアウト計算は別のスレッドで実行され、ネイティブUIビューの更新はメインスレッドで実行されます。これにより、ネイティブとReact Nativeの間で一時的なUIの不一致が発生する可能性があります。これは既知の問題であり、私たちのチームはさまざまなソースからのUI更新の同期に取り組んでいます。
React Nativeは、ルートビューが他のビューのサブビューになるまで、レイアウト計算を実行しません。寸法が判明するまでReact Nativeビューを非表示にする場合は、ルートビューをサブビューとして追加し、最初は非表示にします(UIView
のhidden
プロパティを使用)。次に、デリゲートメソッドで可視性を変更します。