Open14

Expo/React Native でネイティブコード

Takanori IshikawaTakanori Ishikawa

とりあえず、公式の Native Modules NPM Package Setup · React Native を進めてみる。とはいえ、令和の時代に Objective-C を書きたくはないので、できれば Swift でやりたい。

create-react-native-library で Swift & Java のプロジェクトを作って、yarn example ios を実行すると膨大なエラーが出た。

$ yarn example ios
yarn run v1.22.10
$ yarn --cwd example ios
$ react-native run-ios
info Found Xcode workspace "AwesomeModuleExample.xcworkspace"
info Launching iPhone 11 (iOS 15.4)
info Building (using "xcodebuild -workspace AwesomeModuleExample.xcworkspace -configuration Debug -scheme AwesomeModuleExample -destination id=63D7F50B-1490-42E2-AF9C-894D0FE251AC")
error Failed to build iOS project. We ran "xcodebuild" command but it exited with error code 65. To debug build logs further, consider building your app with Xcode.app, by opening AwesomeModuleExample.xcworkspace. Run CLI with --verbose flag for more details.
Command line invocation:
    /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild -workspace AwesomeModuleExample.xcworkspace -configuration Debug -scheme AwesomeModuleExample -destination id=63D7F50B-1490-42E2-AF9C-894D0FE251AC

出力に従って、Xcode でビルドしてみる。同様に失敗して、エラーは以下だった。

fatal error: module map file '/Users/ishikawasonkyou/Library/Developer/Xcode/DerivedData/AwesomeModuleExample-cgzkidotkqodlwgtgpzxsxsppdej/Build/Products/Debug-iphonesimulator/YogaKit/YogaKit.modulemap' not found
1 error generated.

と思ったら、間違えて .xcodeproj を開いていた。ちゃんと .xcworkspace を開いてやり直す。

やっぱりダメ。Flipper-Folly でコンパイルエラー

この人と同じかも。

Flipperってこれかな? debugger らしい。個人的にほとんど debugger は使わないのでコメントアウトしてしまおう

example/ios/Podfile

  # Enables Flipper.
  #
  # Note that if you have use_frameworks! enabled, Flipper will not work and
  # you should disable these next few lines.
  #use_flipper!({ 'Flipper' => '0.80.0' })
  #post_install do |installer|
  #  flipper_post_install(installer)
  #end
Takanori IshikawaTakanori Ishikawa

ダミーの .swift ファイル作って、ライブラリを検索パスに入れても動かないな...

  1. Example ワークスペースのターゲットにダミーの .swift ファイルを追加
  2. yarn example ios で動いた
Takanori IshikawaTakanori Ishikawa

昨日は、公式の手順で進めてうまくいく手順がうまく整理できなかったので、改めてやり直す。言語には "Kotlin & Swift" を選択する。

$ npx create-react-native-library react-native-awesome-module
✔ What is the name of the npm package? … react-native-awesome-module
✔ What is the description for the package? … my lib
✔ What is the name of package author? … xxx
✔ What is the email address for the package author? … xxx
✔ What is the URL for the package author? … xxx
✔ What is the URL for the repository? … xxx
✔ Which languages do you want to use? › Kotlin & Swift
✔ What type of library do you want to develop? › Native module (to expose native APIs)
...
$ cd react-native-awesome-module 

example/ios/Podfile を編集する

diff --git a/example/ios/Podfile b/example/ios/Podfile
index 14377b9..9c08a3a 100644
--- a/example/ios/Podfile
+++ b/example/ios/Podfile
@@ -14,8 +14,8 @@ target 'AwesomeModuleExample' do
   #
   # Note that if you have use_frameworks! enabled, Flipper will not work and
   # you should disable these next few lines.
-  use_flipper!({ 'Flipper' => '0.80.0' })
-  post_install do |installer|
-    flipper_post_install(installer)
-  end
+  #use_flipper!({ 'Flipper' => '0.80.0' })
+  #post_install do |installer|
+  #  flipper_post_install(installer)
+  #end
 end

yarn を実行

$ yarn

これでちゃんと動いた。

Takanori IshikawaTakanori Ishikawa

ダミーの .swift ファイルも自動で作られていた。これで npm パッケージとして動くはず。

Example アプリでは、こういう感じで Podfile に記述されていた。

  pod 'react-native-awesome-module', :path => '../..'

パッケージを使う側のコード example/src/App.tsx

  React.useEffect(() => {
    multiply(3, 7).then(setResult);
  }, []);

実装を少し変えてみる。

diff --git a/ios/AwesomeModule.swift b/ios/AwesomeModule.swift
index 3f8455d..e9bfd9f 100644
--- a/ios/AwesomeModule.swift
+++ b/ios/AwesomeModule.swift
@@ -3,6 +3,6 @@ class AwesomeModule: NSObject {
 
     @objc(multiply:withB:withResolver:withRejecter:)
     func multiply(a: Float, b: Float, resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) -> Void {
-        resolve(a*b)
+        resolve(a+b)
     }
 }

あとは yarn example ios で変更が反映されることを確認した。

Takanori IshikawaTakanori Ishikawa

ios/xxx.xcodeproj を開くと React 関係のヘッダーが見つからない状態なので、とりあえず example 以下のものを入れておく...。

Takanori IshikawaTakanori Ishikawa

定数を公開する場合、constantsToExport() を実装する。また、requiresMainQueueSetup も実装すべきらしい。

For iOS, if you override constantsToExport() then you should also implement + requiresMainQueueSetup to let React Native know if your module needs to be initialized on the main thread, before any JavaScript code executes.

iOS Native Modules · React Native

@objc(RNCoreBluetooth)
class MyNativeModule: NSObject, RCTBridgeModule {
  @objc
  static func moduleName() -> String! {
    "MyNativeModule"
  }
  
  // If your module does not require access to UIKit, then you should respond to
  // + requiresMainQueueSetup with NO.
  @objc
  static func requiresMainQueueSetup() -> Bool {
    false
  }
  
  // Export constants
  @objc
  func constantsToExport() -> [AnyHashable: Any]! {
    return [
      "Constant1": "1.2.3.4.5",
      "Constant2": 2022,
    ]
  }
  ...
}

index.ts

export const {
  Constant1,
  Constant2,
}: {
  Constant1: string;
  Constant2: number;
} = MyNativeModule.getConstants();
Takanori IshikawaTakanori Ishikawa

React Native を最新版にする

react-native-builder-bob で作った雛形はそのままだと古い RN を使っているので、これをアップグレードする。

$ npx react-native upgrade

また、example も作り直す。

$ rm -rf example
$ npx react-native init ExampleApp --directory example

example/ios を編集

Flipper は無効にする

diff --git a/example/ios/Podfile b/example/ios/Podfile
index 637c976..ae7cfc1 100644
--- a/example/ios/Podfile
+++ b/example/ios/Podfile
@@ -28,10 +28,10 @@ target 'ExampleApp' do
   #
   # Note that if you have use_frameworks! enabled, Flipper will not work and
   # you should disable the next line.
-  use_flipper!()
-
-  post_install do |installer|
-    react_native_post_install(installer)
-    __apply_Xcode_12_5_M1_post_install_workaround(installer)
-  end
+  #use_flipper!()
+  #
+  #post_install do |installer|
+  #  react_native_post_install(installer)
+  #  __apply_Xcode_12_5_M1_post_install_workaround(installer)
+  #end
 end

example/metro.config.js を、react-native-builder-bob が生成するものと同じものにする。Metro のバージョンも上がって API が変わっている。参考: https://github.com/ishikawa/react-native-core-bluetooth/blob/main/example/metro.config.js

あとは、TypeScript にしたり ESLint いれたり...。

Takanori IshikawaTakanori Ishikawa

React Native プロジェクトを iPhone で実行しようとしたら次のようなエラー

error The resource `/home/Developer/Workspace/react-native-core-bluetooth/example/index.js` was not found.
Error: The resource `/home/Developer/Workspace/react-native-core-bluetooth/example/index.js` was not found.
    at /home/Developer/Workspace/react-native-core-bluetooth/example/node_modules/metro/src/IncrementalBundler.js:297:24
    at gotStat (node:fs:2641:21)
    at FSReqCallback.oncomplete (node:fs:198:21)
info Run CLI with --verbose flag for more details.
Command PhaseScriptExecution failed with a nonzero exit code

このプロジェクトでは TypeScript を使っており、index.js は含まれていない。

上記 Issue でコメントされている通り、Xcode プロジェクトの Build Phases を確認

set -e

export NODE_BINARY=node
../node_modules/react-native/scripts/react-native-xcode.sh

これを

set -e

export NODE_BINARY=node
../node_modules/react-native/scripts/react-native-xcode.sh

に書き直すことで解決した。

Takanori IshikawaTakanori Ishikawa

React Native のネイティブモジュールは Swift で書くよりも Objective-C の方がいい気がしてきた。

  • ドキュメントも Objective-C 向けが多い
  • C++ と一緒に使える
  • ヘッダー類も Objective-C のものを読んだ方が情報多い
Takanori IshikawaTakanori Ishikawa

create-react-native-library で作った雛形だと、example/metro.config.js には以下の行が含まれているが、

const blacklist = require('metro-config/src/defaults/blacklist');

blacklist は最近のバージョンだと exclusionList に変わっている。

const exclusionList = require('metro-config/src/defaults/exclusionList');

また、resolver.blacklistRE オプションも resolver.blockList になっている。

この設定を残しておかないと、example を動かしたときに react-native がふたつ含まれてしまい、 Event Emitter が動かなくなる(JS 側のメソッドを呼べなくなる?)