Edited at
CureAppDay 12

react-nativeでObjective-C/SwiftのAPIを扱う(Native Modules編)

More than 1 year has passed since last update.


この記事の目的

React Native公式がまだ対応してないAPIを触りたい場合、既存のライブラリのコードを利用したい場合、あるいはマルチスレッドなどを用いたい場面では、iOSのAPIを直接叩ける必要があります。

そこに対して、React Nativeでサポートしているのですが、公式の説明やコード例が十分でない&日本語の資料もなさそうなので、まとめてみます。

※公式が不十分でソースコードを読みつつの理解となるため、正しくない内容になってしまうかもしれない点をご了承ください。(そもそも正解がない)

※筆者はObjective-C/Swift初心者な点もご了承ください。

※Android版はこちらをどうぞ→react-nativeでAndroidのAPIを扱う(Native Modules編)


必要なこと

まずは公式にあるようにObjective-Cのコードを例に取ります。

最終的にはJSコードからiOS APIを叩けるようにしたいですよね。

そのためにReact Nativeでは、


  1. RCTBridgeModuleを継承したクラスを作り、React Nativeのマクロ関数を定義して、クラスをアプリに認識させる

  2. 上記1で作ったクラスで露出させたい関数にもマクロ定義を行う

  3. JSコードで、nativeコードを呼ぶ宣言を書いておく。

以上です。簡単ですね。

ここでは、上記手順の実例として簡単にNSLogとCallbackを使うサンプルを書いてみます。

(なお、ほぼこちらの記事そのまんまです。。:pray: 後ろの方でSwift版とか色々書いているのでご了承ください)

http://hrk-ys.blogspot.jp/2015/04/reactnative-tips.html

ちなみに、ソースコードはこちらに置いています。

https://github.com/uryyyyyyy/RNBindingSample/tree/ios-native-module


RCTBridgeModuleを継承したクラスを作る


MyLog.h

#import <UIKit/UIKit.h>


//①
#import <RCTBridgeModule.h>

//②
@interface MyLog : NSObject <RCTBridgeModule>

@end


①でRCTBridgeModuleをimportしておき、②で、RCTBridgeModuleを継承したクラスを用意するだけです。(Objective-Cでは継承という名前で正しいのかよくわかってないです。。。)

ここでは、他のObjective-Cコードから呼ばれることを想定していないので、特に関数の露出はしません。


マクロを定義し、関数の中身を作る


MyLog.m

#import "MyLog.h"


@implementation MyLog

//①
RCT_EXPORT_MODULE(MyLog);

//②
RCT_EXPORT_METHOD(callFunc:(NSString *)param dict:(NSDictionary*)dict findEvents:(RCTResponseSenderBlock)callback)
{
NSLog(@"param: %@", param);
NSLog(@"dict: %@", dict);

callback(@[ [NSNull null], @{ @"hoge": @"val" } ]);
}

@end


①で、MyLogクラスをReact Nativeから参照できるようにマクロを定義しています。参照時の識別名はデフォルトでもいいのですが、ここではあえて明示しています。JS側のコードではこの名前で利用することが出来ます。

②では、callFuncという名前の関数をEXPORTしています。これによりJS側で使えるようになります。

引数にNSStringなどが入っていますが、これとJSのクラスとの関係はこのようになっています。

Objective-C -> JS

----
BOOL -> boolean
NSNumber -> boolean
NSInteger -> number
float -> number
double -> number
CGFloat -> number
NSNumber -> number
NSString -> string
RCTResponseSenderBlock -> function
NSDictionary -> Object
NSArray -> Array

なお、Androidの方でも同様なのですが、これらの関数はJSと別スレッドで実行されるため返り値を持てません。必要があればCallbackやPromiseを使う必要があります。(くわしくはこちらの記事が分かりやすいかと思います。→[翻訳] Bridging in React Native


JS側でラッパーを用意する

上記の記述で普通にJSで動かせるようになります。ただ、NativeModulesを呼ぶコードがあちこちに散乱するのも嫌なので、大抵はそれをラップしたクラスを噛ませて、アプリではそこからしかNative APIを呼ばないようにしています。(こうすることで、ライブラリ化やAndroid/ios出し分けがしやすくなります。何より責務がちゃんと分かれます。)


MyLog.ios.js

const MyLog = require('NativeModules').MyLog;

export default MyLog;

上記のRCT_EXPORT_MODULEで定義した名前でrequireできます。

これで全ての準備が完了です。


JSから呼んでみる


index.ios.js

...

import MyLog from './MyLog';

...

componentDidMount(){
MyLog.callFunc(
'action',
'string_param1',
{ foo: 'bar'},
(error, ret) => {
if (error) {
console.error(error);
} else {
console.log(ret);
}
}
);
}


簡単ですね。


Swiftコードを呼べるようにする。

イマドキObjective-Cなのも辛いでしょうか? Swiftを使っていきましょう。(こちらはこちらで、まだまだ安定してなくてツラミが有りますが、、、Appleいい加減互換性気にしながら作ってくれよ。。。)

こちらの場合の手順は以下です。

(こちらの記事を参考にしました。ありがとうございます。http://lealog.hateblo.jp/entry/2016/07/21/002553)


  1. Bridging-Headerを用意してObjective-CからSwiftを呼べるようにする。

  2. Objective-CのコードでSwiftコードを呼び、かつそれをマクロでEXPORTする。

  3. Swiftコードの中身を書く

※よく知らないのですが、手元の最新のXcodeでやっているのでSwift3系で動いているハズ。。


Objective-CからSwiftコードを呼べるようにする

Objective-CプロジェクトでSwiftコードをnewするとXcodeが勝手に作ってくれるはずです。


XXX-Bridging-Header.h

#ifndef MyProject_Bridging_Header_h

#define MyProject_Bridging_Header_h

#import "RCTBridgeModule.h"

#endif

注意点としては、ここでRCTBridgeModuleを読み込ませておくことが必要なようです。

原理は書かれていませんが、これでSwiftからJSを叩くこともできるようになるっぽい?


Objective-Cのコードでブリッジする。


MyLogSwift.m

#import <Foundation/Foundation.h>

#import "RCTBridgeModule.h"

//①
@interface RCT_EXTERN_MODULE(MyLogSwift, NSObject)

//②
RCT_EXTERN_METHOD(callFunc:(NSInteger *)typeParam dict:(NSDictionary*)dict findEvents:(RCTResponseSenderBlock)callback)

@end


外部のクラスをモジュールとして呼び出す時は①のように書くようです。第一引数がJSから参照する際の識別名ですね。

②では、JSから参照可能な関数を定義しています。ただ中身はまだ何もない状態です。後述のSwiftコードの方からこちらとの紐付けを行います。


Swiftコードの中身を書く


MyLogSwift.swift

import Foundation

import MediaPlayer

//①
@objc(MyLogSwift)
class MyLogSwift: NSObject {

//②
@objc(callFunc:dict:findEvents:)
func callFunc(typeParam: NSInteger, dict: NSDictionary, callback: RCTResponseSenderBlock) {

let num = (typeParam as Int)
var enumVal = ""
switch num {
case 1: enumVal = "pattern 1"
case 2: enumVal = "pattern 2"
case _: enumVal = "bad pattern"
}
print("typeParam: " + enumVal);

let _dict:Dictionary = dict as Dictionary
print("dict: ");
print(_dict)

let obj: Dictionary<String, String> = ["foo": "bar", "hey": "ho"]

callback([NSNull(), obj]);
}
}


関数の中身は、ここではenum的なことをしたいと思って書いていますが、Swiftを知っている方ならわかるかと思います。

Objective-Cとの連携のために①、②のような定義をしていますが、React Native感の一切ない普通のコードです。


JSから呼び出す

先程とおなじなので特に書くこともありませんが、enum的な記述をしたければこのように書けます。


MyLogSwift.ios.js

'use strict';

const MyLogSwift = require('NativeModules').MyLogSwift;

export default MyLogSwift;

export const PATTERN1 = 1
export const PATTERN2 = 2



index.ios.js

import MyLogSwift, {PATTERN1} from'./MyLogSwift';

...

MyLogSwift.callFunc(
PATTERN1,
{ foo: 'bar'},
(error, ret) => {
if (error) {
console.error(error);
} else {
console.log(ret);
}
}
);



enumを使いたい

Androidと違い、Objective-Cの方ではそのような仕組みが貧弱です。

(一応RCT_ENUM_CONVERTERがありますが、JS側から呼び出す時には普通の文字列っぽくなって残念な感じです。)

そのまま使ってもいいですし、Swiftの例で書いたような、JSのラッパーの方で吸収する方法でも良いのではと思います。


まとめ

上記のようにして、比較的簡単にNative APIを触れるようになりました。

React Nativeの3rd partyライブラリを見ていてもObjective-Cのコードが多いですが、自分で書くならSwiftのコードの方が使いやすいかなと思っています。(使う側がbindingを用意する必要がありますが、それくらいやってもいいかなと。)

他にも、PromiseやThreading、Events発火、アプリのライフサイクル管理なども組み込むことが出来るそうです。折をみてこちらの資料も更新していければと思っています。