##この記事の対象
この記事は、
- ReactNativeアプリを作っていて
- iOS側でネイティブを実装する必要があり
- かつそれをSwiftで書きたいんだけど
- Swiftもネイティブのブリッジの仕方も分からない!
という方が対象です。
かく言う私がそうでして、ここで書かれているコードは動くものの、間違いはあるかもしれませんのでお気をつけください!!
(私の場合、とあるサービスの実装ガイドがSwift版しかなく、jsでの実装が難しそうだったので泣く泣くSwiftでネイティブを実装しました。。。)
##この記事で分かるようになる(と願う)こと
数を増やしたり減らしたりするカウンターアプリを通して、以下3つを共有できればと思います。
- ネイティブ側でSwift使うのに必要な最低限のファイル3つ(
.swift
,.m
,.h
)の最低限の書き方 - ネイティブ側からjs側への値の渡し方(コールバックとプロミスの2パターン)
- js側で値を受け取り方(同様にコールバックとプロミスの2パターン)
##この記事で説明しないこと
- ネイティブ実装の諸々の詳細(使える型とか、その他のパターンとか)
- React Nativeのプロジェクトの作り方
- React Nativeのjs部分の書き方(stateの概念とか更新の仕方とか)
- Androidのネイティブ実装
##手っ取り早くソース見たい方は
GitHubに上げてるのでこちらをご覧ください。
なお、この記事は以下の記事の簡易版なので、詳細はそちらを参考にしてください。
環境
System:
OS: macOS 10.14
Binaries:
Node: 10.5.0 - ~/.nodebrew/current/bin/node
Yarn: 1.12.3 - /usr/local/bin/yarn
npm: 6.1.0 - ~/.nodebrew/current/bin/npm
Watchman: 4.9.0 - /usr/local/bin/watchman
SDKs:
iOS SDK:
Platforms: iOS 12.1, macOS 10.14, tvOS 12.1, watchOS 5.1
IDEs:
Xcode: 10.1/10B61 - /usr/bin/xcodebuild
npmPackages:
react: 16.6.1 => 16.6.1
react-native: 0.57.7 => 0.57.7
npmGlobalPackages:
create-react-native-app: 1.0.0
react-native-cli: 2.0.1
react-native-git-upgrade: 0.2.7
あとSwiftは3.0
に指定しています。
##この記事でやること
簡単なカウンターアプリの作り方の紹介を通じてネイティブ実装の方法を共有します。
機能は以下3つです。
1. 数を1増やす (Increment)
2. 数を1減らす (Decrement)
3. 数が0未満になる時にはエラーメッセージを表示する
すごく単純ですね。
js側ではstateで現在の値(初期値は0)を持っていて、その現在値をネイティブ側に渡し、swiftコードで足したり引いたりした値をjs側で受け取って、stateを更新し画面を再描画します。
js(現在の値) -> swift(計算) -> js(計算後の値)
また、ネイティブ側からjs側に値を渡す時ですが、
- Incrementの時はコールバック
- Decrementの時はプロミス
を使っています。
それでは、以下の順に見ていきます。
- ネイティブ側で必要な最低限のファイル3つ(
.swift
,.m
,.h
)の最低限の書き方 - ネイティブ側からjs側への値の渡し方(コールバックとプロミスの2パターン)
- js側で値を受け取り方(同様にコールバックとプロミスの2パターン)
ネイティブ側で必要な最低限のファイル3つ
Swiftでネイティブコードを書くには、最低限以下三つのファイルが必要です。
- Swiftファイル(
.swift
) - Objective-C Bridging Headerファイル(
.h
) - Objective-C Macrosファイル(
.m
)
Macrosがなんなのかとか、私は全然分かっておりません。。
1. Swiftファイルを作る
- ReactNativeプロジェクトのiOSディレクトリを、Xcodeで開く
-
AppDelegete.h
などがある階層に、New File
で新規ファイルを作る(Swift File を選択)
※ この時に「Objective-C bridging headerを作りますか?」的なことを聞かれたら作成してください。
SampleProject
∟ SampleProject <- ここのディレクトリにSwiftファイルを新規追加
∟ Counter.swift <- 追加したSwiftファイル
∟ Libraries
∟
以上!
2. Objective-C Bridging Headerファイルを作る
Swiftファイルをjs側にブリッジするために、 Objective-C Bridging Header
ファイルというのが必要なようで。SwiftだけじゃなくObjective-Cも書かないといけないんすね。
ここのステップは、先ほどSwiftファイルを作った段階で「「Objective-C bridging headerを作りますか?」で作成した方はもう完了です。
ただ、私の場合は聞かれないこともありました。その場合は、自分で作る必要があります。
まずファイルを作る前に、ファイル名を確認します。
基本的には、
プロジェクト名-Bridging-Header.h
です。
SampleProject-Bridging-Header.h
など。
TargetのBuild Settings
からもObjective-C Bridging Header
という項目でファイル名を確認できます。
上記で確認したファイル名で、新規ファイルを作成します。
ファイル作成時にはHeader Fileを選択します。
SampleProject
∟ SampleProject
∟ Counter.swift
∟ SampleProject-Bridging-Header.h <- 追加したヘッダーファイル
∟ Libraries
∟
完了!
###3. Objective-C Macrosファイルを作る
これまでと同様にして新規ファイルを作ります。
今度は、Objective-C Fileを選択してください。
SampleProject
∟ SampleProject
∟ Counter.swift
∟ SampleProject-Bridging-Header.h
∟ Counter.m <- 追加したObj-Cファイル
∟ Libraries
∟
完了!
ネイティブ側からjs側への値の渡し方
ここからが本題。
ネイティブ側を書いていきます。
以下の順番で見ていきます。
Swift
- Counterクラスの用意
- Incrementメソッド実装(コールバック使う)
- Decrementメソッド実装(プロミス使う)
Objective-C(.m)
4. Counterクラスのエクスポート
5. Incrementメソッドのエクスポート
6. Decrementメソッドのエクスポート
ブリッジヘッダー(.h)
7. ブリッジを完成させる
###1. Counterクラスの用意
Swiftファイル
import Foundation
@objc(Counter)
class Counter: NSObject {
}
Swiftの書き方分からないので、おまじないだと思ってください。。
Objective-Cに読み取ってもらうためには@objc
が大事だそうです。
###2. Incrementメソッド実装(コールバック使う)
SwiftファイルのCounterクラス内に以下を追加します。
@objc
func increment(_ origin: Int, withCallback callback: RCTResponseSenderBlock) -> Void {
callback([origin + 1])
}
入力値に1を加えた数を、コールバックで返すようにしています。
簡単に説明すると、
origin: Int
=>引数1で計算元の数値。
withCallback callback: RCTResponseSenderBlock
=>引数2でコールバック。 withCallback
は呼び出し元に表示される引数名で、 callback
がメソッド内で使う引数名。
callback([origin + 1])
=>計算元に1加えた数を引数に、コールバックを呼び出し。
コールバック呼び出し時の引数は、引数が一個でも配列にする。
また、このメソッド自体は返り値がないです。
以下みたいに普通にメソッドの返り値使えばいいじゃんって私は思ったのですが、ReactNativeのブリッジではそれはNGのようです。Androidも同じです。なので、js側に値を渡すために、コールバックかプロミスを使っています(他にも方法あるのかもしれませんが)。
@objc
func increment(_ origin: Int) -> Int {
return origin + 1
}
###3. Decrementメソッド実装(プロミス使う)
同様に以下を追加します。
@objc
func decrement(_ origin: Int, withResolve resolve: RCTPromiseResolveBlock, withReject reject: RCTPromiseRejectBlock) -> Void{
if(origin <= 0){
reject("E_COUNT", "You can't decrement any more!!", nil)
return
}
resolve(origin - 1)
}
計算元の数値が0以下の場合にはエラーを返すようにしています。
それ以外の場合には、計算元から1引いた数をプロミスの渡し手に返します(今回は配列じゃないです)。
エラーハンドリングはよくわかってないので誤魔化しています(nilの部分とか)。
プロミスって何よっていうのは、私にはうまく説明することができないので、ググってください。。
これでSwiftファイルは完成です。
全体はこちらをご確認ください。
補足
上記ファイルには
static func requiresMainQueueSetup() -> Bool {
return false
}
というのがあります。
これは、「requiresMainQueueSetupを実行しろ!」という警告が出るのを防ぐためです。非同期でもいい場合は false
を返すそうです。詳細は分かりません。すみません。
###4. Counterクラスのエクスポート
ここから、Objective-Cファイル(Counter.m
)をいじっていきます。
先ほどのCounter.swift
のCounterクラスをjs側にエクスポートするには、
#import <Foundation/Foundation.h>
#import "React/RCTBridgeModule.h"
@interface RCT_EXTERN_MODULE(Counter, NSObject)
@end
と書きます。
Counter(Swiftのやつ)をエクスポートします、これはオブジェクトです、という感じです。
Counter.swift
で
@objc(Counter)
class Counter: NSObject {
}
としましたが、 この@objc(Counter)
の部分と紐づいているみたいです(ただの予想です。。)
###5. Incrementメソッドのエクスポート
Swiftクラスのincrement
メソッドをエクスポートします。
@interface RCT_EXTERN_MODULE(Counter, NSObject)
//↓を追加
RCT_EXTERN_METHOD(increment: (NSInteger)origin withCallback:(RCTResponseSenderBlock)callback)
@end
###6. Decrementメソッドのエクスポート
Swiftクラスのdecrement
メソッドをエクスポートします。
@interface RCT_EXTERN_MODULE(Counter, NSObject)
RCT_EXTERN_METHOD(increment: (NSInteger)origin withCallback:(RCTResponseSenderBlock)callback)
// ↓を追加
RCT_EXTERN_METHOD(decrement: (NSInteger)origin withResolve:(RCTPromiseResolveBlock)resolve withReject:(RCTPromiseRejectBlock)reject)
@end
特に説明なくてすみません。
引数とかは、Swiftファイルと見比べてください。
###7. ブリッジを完成させる
Native側の最後は、Bridging-Headerファイルです。
#import "React/RCTBridgeModule.h"
はい、この一行で大丈夫です。
##js側での値を受け取り方
さぁ、ようやくjs側まで来ました。
App.js
クラスをいじります。
レイアウトとかそこらへんは触れません。
前提
初期stateは以下にします。
constructor(props) {
super(props)
this.state = {
count: 0,
errorText: ""
}
}
###1. NativeModulesをimportする
import {
Platform,
StyleSheet,
Text,
View,
//↓↓↓
NativeModules,
Button
} from "react-native"
###2. incrementメソッドを呼び出して値を受け取る
onPressIncrement = () => {
const {count} = this.state
const callBack = incrementedCount => {
this.setState({count: incrementedCount, errorText: ""})
}
NativeModules.Counter.increment(count, callBack)
}
コールバックを定義し、今の値とコールバックを引数にしてNativeModules.Counter.increment
を呼び出します。
incrementedCount
が、swiftのincrementメソッド内で1足された数値です。
###3. dncrementメソッドを呼び出して値を受け取る
onPressDecrement = () => {
const {count} = this.state
NativeModules.Counter.decrement(count)
.then(decrementedCount => this.setState({count: decrementedCount}))
.catch(error => {
this.setState({errorText: error.message})
})
}
または async/awaitを使うのであれば以下のようにも書けます。
onPressDecrement = async () => {
const {count} = this.state
const decrementedCount = await NativeModules.Counter.decrement(count).catch(error =>
this.setState({errorText: error.message})
)
if (decrementedCount) {
this.setState({count: decrementedCount})
}
}
ネイティブの方では以下のように引数3つですが、js側では decrement(count)
と1つだけ。不思議ですが、プロミスの場合はよろしくやってくれるようです。
@objc
// 引数3つ
func decrement(_ origin: Int, withResolve resolve: RCTPromiseResolveBlock, withReject reject: RCTPromiseRejectBlock) -> Void{
///
}
// 引数1つ
NativeModules.Counter.decrement(count)
さて、後半スピードアップした、または説明を面倒がりましたが、Swiftを使ってのネイティブ実装は以上です。
改めて、ソース全体はこちらでご確認ください。
##参考サイト