Cover image for react-nativeでNative moduleを呼び出す(Swift編)
react-nativeswift

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

February 04, 2019

8 min read

mitsuruogMitsuru Ogawa

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 設計が重要な気がします。