【React Native】Native Modules: iOS の書き方

2022/06/18に公開1

Native Modules というAPIを利用することで、Swift, Objective-C や Java のコードを呼び出すことができます。これによって、以下が可能です。

  • JavaScriptから利用できない iOS/Android のAPIを使用する
  • 既存の Objective-C、Swift、Java、C++ ライブラリを 再利用する

ポイント

  • iOSの Native Moduleを作成する
  • Swift, Objective-C のコードを呼び出す
  • JavaScriptから引数を受け取る
  • JavaScriptへ返り値を返す
  • iOS側で起こるイベントをJavaScriptでsubscribeする

ReactNativeプロジェクトの作成

まだプロジェクトがない場合は、作成します。

npx react-native init myApp

Swiftファイルの作成

  • ios/myApp.xcodeprojにあるプロジェクトをXCodeで開きます。
  • NativeModulesフォルダを作成します。(任意)
  • NativeModulesフォルダ内に.swiftファイルを作成します。

今回は counter.swift というファイルを作成しました。

Objective-C Bridging Headerの作成

.swiftファイルを作成すると、「Objective-C Bridging Headerを作成しますか?」 とダイアログが表示されるので、作成します。

このファイルは、名前の通りSwiftファイルとObjective-Cファイルをブリッジするものです。
ファイル名は変えてはいけません。

以下のように、Objective-C Bridging Headerに追記します。

myApp-Bridging-Header.h
// myApp-Bridging-Header.h

#import"React/RCTBridgeModule.h"

モジュールの実装

簡単なネイティブモジュールを実装してみましょう。
カウンターの値を変化させるモジュールで、以下を含んでいます。

  • Counter クラス
  • add メソッド
counter.swift
import Foundation

@objc(Counter)
class Counter: NSObject {
  private var count = 0

  @objc
  func add() {
    count += 1
    print("count is \(count)")
  }
}

モジュールをReactNativeに露出する

実装したモジュールをReactNativeから呼び出すために、Objective-Cのファイルを作成します。
先ほど作成したSwiftファイルと同じ名前 で、同ディレクトリに作成します。

例えば先程 counter.swift という .swift ファイルを作成したので、counter.m という名前で新しくObjective-Cのファイルを作成します。

作成したファイルに以下を追記します。

counter.m
#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(Counter, NSObject)
  RCT_EXTERN_METHOD(add)
@end
  • RCT_EXTERN_MODULE: モジュールを外部参照可能にします。
    • 上記の例では、Counterクラスを指定しています。
  • RCT_EXTERN_METHOD: メソッドを外部参照可能にします。
    • Counterに実装された addメソッドを指定しています。

ReactNativeから呼び出す

NativeModules をインポートし、クラス名.メソッド名 で呼び出すことができます。

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

NativeModules.Counter.add();

引数を受け取る

addメソッドが引数を受けとる場合は、以下のように記述します。

Swiftファイル

counter.swift
  @objc
  func add(_ num: Int) {
    count += num
    print("count is \(count)")
  }

Obj-Cファイル

引数を受け取る場合、Obj-Cファイルも変更が必要です。

counter.m
@interface RCT_EXTERN_MODULE(Counter, NSObject)
  RCT_EXTERN_METHOD(add: (NSInteger)num)
@end

メソッド名: (型)引数名 に対応しています。

ReactNativeから呼び出す

引数を与えることができます。

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

NativeModules.Counter.add(123);

返り値を返す

iOSの世界から、JavaScript(ReactNative)の世界に、返り値を返す場合は、Promiseを使用する実装になります。
以下の特殊な関数を引数に追加し、それぞれ 正常終了時の返り値エラー時の返り値 に使用できます。

タイミング コールバック
RTCPromiseResolveBlock 処理成功時 then 句
RTCPromiseRejectBlock 処理失敗時 catch 句

Swiftファイル

counter.swift
  @objc
  func add(
    _ num: Int,
    resolver resolve: RTCPromiseResolveBlock, // 追加
    rejecter reject: RTCPromiseRejectBlock // 追加
  ) {
    do {
      count += num
      resolve(count) // 成功
    }
    catch {
      reject("Error!", NSError(...)) // 失敗
    }
  }

Obj-Cファイル

Obj-Cファイルも変更が必要です。

counter.m
@interface RCT_EXTERN_MODULE(Counter, NSObject)
  RCT_EXTERN_METHOD(
    add: (NSInteger)num
    resolver: (RTCPromiseResolveBlock)resolve // 追加
    rejecter: (RTCPromiseRejectBlock)reject // 追加
  )
@end

ReactNativeから呼び出す

.then.catch のコールバックで、返り値を扱うことができます。

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

NativeModules.Counter.add(123)
  .then((res) => console.log(res))
  .catch((err) => console.error(err));

Event Emitterの作成

iOSネイティブ側で起こるイベントをSunscribeしたい時には、RCTEventEmitterを使用します。

Obj-Cファイル

Objective-Cのファイルに、以下を記述します。

counter.m
#import "React/RCTBridgeModule.h"
#import "React/RCTEventEmitter.h"

@interface RCT_EXTERN_MODULE(Counter, RCTEventEmitter)

Bridging-Header ヘッダファイル

さらに、ブリッジヘッダファイルにも追記します。

myApp-Bridging-Header.h
#import "React/RCTBridgeModule.h"
#import "React/RCTEventEmitter.h"

Swift ファイル

Swift側では以下のような実装を行います。

counter.swift
@objc(Counter)
class Counter: RCTEventEmitter {
  @objc
  func increment() {
    count += 1
    print("count is \(count)")
    sendEvent(withName: "onIncrement", body: ["count": count])
  }

  override func supportedEvents() -> [String]! {
    return ["onIncrement"]
  }
}

ReactNativekからイベントリスナーで待ち受ける

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

const CounterEvents = new NativeEventEmitter(NativeModules.Counter);

CounterEvents.addListener(
  "onIncrement",
  (res) => console.log(res)
);

NativeModules.Counter.increment();

参考

https://reactnative.dev/docs/native-modules-intro
https://medium.com/@andrei.pfeiffer/react-natives-rct-extern-method-c61c17bf17b2
https://mo-gu-mo-gu.com/react-native-modules-ios-swift/

Discussion

kjiroukjirou

文中にいくつか RTCPromiseResolveBlockRTCPromiseRejectBlock という誤字があるようです...!
TとCが逆になっている。