react-nativeでNative moduleを呼び出す(Swift編)
2019-02-05

react-nativeでNative moduleを呼び出す(Swift編)

react-nativeでSwiftのNative moduleを呼び出す方法です。基本的には下のBlogのやり方を真似ています。

紹介する内容は次の通りです。

  • 簡単なCounterをNative Moduleで実装した
  • Native Moduleの呼び出し
  • Native ModuleからConstantsを受け取る
  • Native ModuleからのCallbackを扱う
  • Native ModuleからのPromiseを扱う
  • Native ModuleからのEventを扱う

対象のバージョンは次の通りです。

  • react-native: 0.57.8
  • Swift: 4.2.1
  • Xcode: 10.1

プロジェクト全体のコードはGitHubで見ることができます。

ちなみにSwiftとObjecvive-Cは初めて書きました。

Native Moduleの呼び出し

まず、Counter.swiftというSwiftのクラスを作成します。
この時にObjective-C Bridging Headerの設定をするか確認されるので、「Create Bridging Header」を選択してBride Headerファイルを作成します。

このようなダイアログが表示されるはずです。

Bride Headerファイルは、一度設定されるとXcodeのプロジェクトファイルで管理されているため、手動でファイル名などを変更することは避けましょう。

Bride Headerファイルにreact-nativeのモジュールをインポートしておきます。

// Bridging-Header.h

#import "React/RCTBridgeModule.h"

続いて、SwiftクラスにCounterクラスを定義します。

// Counter.swift

import Foundation

@objc(Counter)
class Counter: NSObject {
}

次にObjective-Cのファイルを作成して、Native ModuleをJavaScript側に公開するためのマクロを登録します。

RCT_EXTERN_MODULEの最初の引数がJavaScript側に公開される名前で、第2引数がNative ModuleのSuper Classを渡します。

// Counter.m

#import "React/RCTBridgeModule.h"

@interface RCT_EXTERN_MODULE(Counter, NSObject)
@end

react-native側ではNativeModulesの中に、先ほど定義したモジュール名でNative Moduleが渡されてくるので、これを利用します。

// App.js
import { NativeModules } from 'react-native';

const { Counter } = NativeModules;

// 何かの処理
// Counter.doSomething();

これでNative Moduleをreact-native側で利用する準備が整いました。

Native ModuleからConstantsを受け取る

Native Module側からcounterの初期値を返します。

Counter.swiftconstantsToExportメソッドを追加します。react-native側に渡したいものをdictionaryの中に設定していきます。

続いてrequiresMainQueueSetupメソッドも追加します。これはこのクラスの初期化をメインスレッドかバックグラウンドスレッドのどちらで行うかを指定するためのものです。
何も指定しない場合、次のような警告が表示されます。

// Counter.swift

  ...

  @objc
  override func constantsToExport() -> [AnyHashable : Any]! {
    return ["initialCount": 0]
  }

  @objc
  static func requiresMainQueueSetup() -> Bool {
    // true  - メインスレッドで初期化される
    // false - メバックグラウンドスレッドで初期化される
    return true
  }
}

react-native側では、initialCountは次のように利用することができます。

// App.js
console.log(Counter.initialCount); // => 0

Native ModuleからのCallbackを扱う

現在のcountを返すgetCountメソッドを実装します。

CallbackはRCTResponseSenderBlockクラスで定義されているので、これを引数で受け取ってCallbackを実行します。

// Counter.swift

@objc(Counter)
class Counter: RCTEventEmitter {

  private var count = 0

  ...

  @objc
  func getCount(_ callback: RCTResponseSenderBlock) {
    callback([count])
  }
}

続いてCounter.mにメソッドを追加します。

// Counter.m

@interface RCT_EXTERN_MODULE(Counter, NSObject)
  RCT_EXTERN_METHOD(getCount: (RCTResponseSenderBlock)callback)
@end

react-native側では次のように利用します。

// App.js
Counter.getCount(count => console.log(count)); // => 0

Native ModuleからのPromiseを扱う

次はPromiseを扱ってみます。

decrementメソッドを実装します。正しく減算できた場合はresolveを、countが0で減算しようとした場合にrejectを返すようにします。

Promiseはresolveの場合のRCTPromiseResolveBlockrejectの場合のRCTPromiseRejectBlockを利用します。

rejectする場合は、第3引数にErrorオブジェクトが必要なので、NSErrorでエラーを作成しておきます。

// Counter.swift

@objc(Counter)
class Counter: NSObject {

  ...

  @objc
  func decrement(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) -> Void {
    if (count == 0) {
      let error = NSError(domain: "", code: 200, userInfo: nil)
      reject("E_COUNT", "count count cannot be negative.", error)
    } else {
      count -= 1
      resolve("count was decremented.")
    }
  }
}

続いてCounter.mにメソッドを追加します。

// Counter.m

@interface RCT_EXTERN_MODULE(Counter, NSObject)
  RCT_EXTERN_METHOD(getCount: (RCTResponseSenderBlock)callback)
+  RCT_EXTERN_METHOD(decrement: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject)
@end

react-native側では通常のPromiseと同じように扱うことができます。

// App.js
Counter.decrement()
  .then(count => console.log(count))
  .catch(error => console.error(error));

Native ModuleからEventを受け取る

最後にdecrementした時に、onDecrementイベントが発火するようにして、これをreact-native側で利用できるようにします。

Eventをreact-native側に送るにはRCTEventEmitterが必要なので、Counter.mを変更してインポートしておきます。

// Counter.m
#import "React/RCTBridgeModule.h"
+ #import "React/RCTEventEmitter.h"

- @interface RCT_EXTERN_MODULE(Counter, NSObject)
+ @interface RCT_EXTERN_MODULE(Counter, RCTEventEmitter)
  ...
@end

続いてCounter.swiftを変更します。

まずクラスをRCTEventEmitterのサブクラスにします。次にsupportedEventsを実装して、Native Moduleから発火されるイベント名を返すようにします。
最後にrequiresMainQueueSetupをoverrideに変更します。

// Counter.swift

@objc(Counter)
- class Counter: NSObject {
+ class Counter: RCTEventEmitter {

  ...

+  @objc
+  override func supportedEvents() -> [String]! {
+    return ["onDecrement"]
+  }

  @objc
-  static func requiresMainQueueSetup() -> Bool {
+  override static func requiresMainQueueSetup() -> Bool {
    ...
  }

  ...

}

react-native側にイベントを送るにはsendEventを使います。送るペイロードはMapを使って準備します。

// Counter.swift

  ...

  @objc
  func decrement(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) -> Void {
    if (count == 0) {
      let error = NSError(domain: "", code: 200, userInfo: nil)
      reject("E_COUNT", "count count cannot be negative.", error)
    } else {
      count -= 1
+      sendEvent(withName: "onDecrement", body: ["count": count])
      resolve("count was decremented.")
    }
  }
}

react-native側ではNativeEventEmitterの中にNative Moduleのインスタンスを設定してEventEmitterを取得します。
あとは、EventEmitterにEventListenerを設定すればOKです。

// App.js
import { NativeModules, NativeEventEmitter } from 'react-native';

const counterEventEmitter = new NativeEventEmitter(Counter);

counterEventEmitter.addListener('onDecrement', ({ count }) => {
  console.log(count); // => 1
});

まとめ

react-nativeでSwiftのNative moduleを呼び出す方法についてでした。

Objective-Cを書いていたら、遠い昔に触って挫折した苦い記憶が蘇ってきました。

JavaScript側はAndroidと同じ形で処理できるので、マルチプラットフォームのNative Moduleを扱う場合は、両者を等しく扱えるようなI/F設計が重要な気がします。