Expo/React Native でネイティブコード
Expo/React Native でネイティブコードを使うためにやったことの覚え書き。特に、npm パッケージとして公開する場合。
とりあえず、公式の 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
今度はリンク時に大量のエラーが出た。
明らかに関係ありそうな警告
ダミーの .swift ファイル作って、ライブラリを検索パスに入れても動かないな...
- Example ワークスペースのターゲットにダミーの .swift ファイルを追加
-
yarn example ios
で動いた
しかし、雛形作成に使っている react-native-builder-bob の使ってる RN が古すぎる (0.63.4)...。Microsoft の React Native Developer Tools が使えるのかな?
昨日は、公式の手順で進めてうまくいく手順がうまく整理できなかったので、改めてやり直す。言語には "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
これでちゃんと動いた。
ダミーの .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
で変更が反映されることを確認した。
ios/xxx.xcodeproj
を開くと React 関係のヘッダーが見つからない状態なので、とりあえず example 以下のものを入れておく...。
定数を公開する場合、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();
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 いれたり...。
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
に書き直すことで解決した。
React Native のネイティブモジュールは Swift で書くよりも Objective-C の方がいい気がしてきた。
- ドキュメントも Objective-C 向けが多い
- C++ と一緒に使える
- ヘッダー類も Objective-C のものを読んだ方が情報多い
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 側のメソッドを呼べなくなる?)