Cocoapodsで入れたライブラリをReact Nativeとbridgeして使う

この記事ではCocoapodsで導入したライブラリをReact Native側から呼び出して利用する方法についてご紹介します。
今回は、今更Objective-CでNative側のコード書くのもなんだかなー、と思いSwiftで書きました。

React Native側のボタンを押すと、Whisperというライブラリが呼び出される、ということを試しました。
こんな見た目の奴です。

Whisper
https://github.com/hyperoslo/Whisper


今回のソースはこちらに置いてあるのでよければ参考にしてください。
https://github.com/kouchi67/CocoapodsReactBridge


では、ハマったポイントも含めて説明していきます。


## 環境

- OS: El Capitan (10.11.6)
- Xcode: 8.1
- react native 0.38.1


## Project作成

React NativeのProjectを作成します。

```shell-session
$ react-native init CocoapodsReactBridge
```

## Cocoapods導入

Cocoapodsを導入していきます。

```shell-session
$ cd CocoapodsReactBridge/ios
$ pod init
$ vim Podfile
```

```rb:Podfile
platform :ios, '10.0'
use_frameworks!

target 'CocoapodsReactBridge' do
  pod 'Whisper', '~> 3.1'
end
```

これを書いている現在では、 Whisperの最新Versionは 4.0 でしたが、
Swift 3 でビルドできなかったので、 Swift 2.3 で動作する Version を導入しています。

Podfileの記述が完了したら、ライブラリをインストールします。

```shell-session
$ pod install
```

pod installが完了したらXcodeでプロジェクトを開きます。

```shell-session
$ open CocoapodsReactBridge.xcworkspace
```

## Bridgeクラスの作成

Native側の処理をReact Nativeから呼べるように準備します。
以下のファイルを作成します。

- ReactWhisper.m
- ReactWhisper.swift
- CocoapodsReactBridge-Bridging-Header.h

`CocoapodsReactBridge-Bridging-Header.h` に関しては、
Swiftファイルを作成するときに「Bridging-Headerファイル作る？」って聞かれるので、その際に作成しましょう。

順番にファイルを解説します。

```swift:ReactWhisper.swift
import UIKit
import Whisper

@objc(ReactWhisper)
class ReactWhisper: NSObject {
  
  @objc func showWhisper(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
   
    dispatch_async(dispatch_get_main_queue()) {
      let murmur = Murmur(title: "Called Native Modules !!")
      show(whistle: murmur, action: .Show(2.0))
    }

    resolve([])
  }

}
```

swiftファイルは、ライブラリを呼び出す本体のクラスとなります。
クラスとメソッドを宣言するところで、 `@objc` とつけることで、Objective-Cから呼び出せるようになります。
React NativeとNative ModulesのBridging部分はObjective-Cで書く必要があるため、 `@objc` が必要です。
（Bridging部分もSwiftで書けるやり方があれば教えてください・・・）

```objc:ReactWhisper.m
#import <Foundation/Foundation.h>
#import "RCTBridgeModule.h"

@interface RCT_EXTERN_MODULE(ReactWhisper, NSObject)

RCT_EXTERN_METHOD(showWhisper: (RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)

@end
```

`ReactWhisper.m` では、React NativeとNative ModulesのBridgingについて記述します。
`RCT_EXTERN_MODULE` `RCT_EXTERN_METHOD` を使うことで、Native側のクラスやメソッドをReact Nativeから参照できるようになるようです。
今回は `ReactWhisper.swift` で記述した `showWhisper` メソッドを呼び出せるようにします。


```c:CocoapodsReactBridge-Bridging-Header.h
#import "RCTBridgeModule.h"
```

このファイルは、SwiftからObjective-Cのクラスを参照するために必要です。
Swiftから `RCTPromiseResolveBlock` など参照する必要があるため、必要です。


## Swift 2.3で動くようにする

今のままではビルドが通らないので、Swift 2.3で動作するように設定を変えます。
ビルド設定にある `Use Legacy Swift Version` という項目を `Yes` に修正します。
以下のようになります。

![image](https://qiita-image-store.s3.amazonaws.com/0/2920/19fbe4e1-c810-1b52-213a-f116c1e6653f.png)

これでビルドが通るようになりました。
Native側の修正はこれで一旦終わりです。


## React NativeからNative Modulesを呼び出す

`index.ios.js` に必要最低限なコードが記述されてますが、ここにWhisperを呼び出すためのボタンを追加します。

```js:index.ios.js
import React, { Component } from 'react';
import {
  AppRegistry,
  StyleSheet,
  Text,
  Button,
  NativeModules,
  View
} from 'react-native';

export default class CocoapodsReactBridge extends Component {

  onPressShowWhisper() {
    NativeModules.ReactWhisper.showWhisper();
  }

  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          Welcome to React Native!
        </Text>
        <Text style={styles.instructions}>
          To get started, edit index.ios.js
        </Text>
        <Text style={styles.instructions}>
          Press Cmd+R to reload,{'\n'}
          Cmd+D or shake for dev menu
        </Text>
        <Button
          onPress={this.onPressShowWhisper}
          title="show Whisper"
          color="#841584"
        />
      </View>
      );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
});

AppRegistry.registerComponent('CocoapodsReactBridge', () => CocoapodsReactBridge);
```

`NatveModules` をimportして使用しているところがミソです。

ここまで書けたらXcodeからRunします。
iOS Simulatorが起動できたら、 `show Whisper` ボタンを押してみましょう。

<img width="375" alt="Simulator Screen Shot 2016.12.07 19.18.14.png" src="https://qiita-image-store.s3.amazonaws.com/0/2920/7928d20d-9437-4315-80b8-9d75c450c27d.png">

Status BarのところにWhisperが表示されていますね。（背景が白でわかりにくいですが・・・）
無事に呼び出すことに成功したようです。


## Warningの対応

useNativeDrive is not supported とか表示されてます。
これについてちょっとよくわかってないですが、デフォルトではNative側で動作するアニメーションは対応していないようです。
Simulator上では普通に動いてますが、消したほうがよいのでしょう。。。
とりあえずぐぐったらこんなページを発見しました。

https://github.com/facebook/react-native/issues/11094#issuecomment-263240420

> 1. find RCTAnimation.xcodeproj from node_modules/react-native/NativeAnimation folder
> 2. open xcode
> 3. open project navigator (folder icon from the left)
> 4. drag & drop RCTAnimation.xcodeproj from the previous step to Libraries folder
> 5. Expand RCTAnimation.xcodeproj in project navigator and find libRCTAnimation.a file inside of Products folder
> 6. click on your project name on top of project navigator and go to Build Phrases tab
> 7. drag and drop libRCTAnimation.a to Link Binary With Libraries
> 8. Clean and run :)

こちらの手順に従って、 `libRCTAnimation.a` とやらをXcodeのProjectに追加してあげると良いようです。

RCTAnimation.xcodeprojとやらをまず探します。
私の環境では以下に保存されていました。

```
CocoapodsReactBridge/node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj
```

中を確認しても `.a` ファイルが存在しませんでした。
手順とは違いますが、このプロジェクトでビルドすると `.a` ファイルが作成できるはずなので、Xcodeで開きます。

```
$ cd CocoapodsReactBridge/node_modules/react-native/Libraries/NativeAnimation
$ open RCTAnimation.xcodeproj
```

開いたらビルドしてみます。

![image](https://qiita-image-store.s3.amazonaws.com/0/2920/64a2865a-523e-f83b-f585-0f158f400caa.png)

無事に作成されたようです。

あとは、このファイルをCocoapodsReactBridgeプロジェクトにドラッグ＆ドロップで追加します。
こんなファイル構成になりました。

![image](https://qiita-image-store.s3.amazonaws.com/0/2920/95430a4b-6b9e-3fb1-142a-c374914fb5b8.png)

これでもう一度Runしてみると、Warningが消えていることが確認できると思います。

<img width="375" alt="Simulator Screen Shot 2016.12.07 19.42.18.png" src="https://qiita-image-store.s3.amazonaws.com/0/2920/f59e3100-08fc-1b26-07b2-60b74d37e738.png">

ちゃんとWarningが消えてますね！

## 注意点

swiftファイルの以下の記述について補足します。

```swift
    dispatch_async(dispatch_get_main_queue()) {
      let murmur = Murmur(title: "Called Native Modules !!")
      show(whistle: murmur, action: .Show(2.0))
    }
```

`dispatch_async( ... ` という記述がありますが、これはUI操作に関する処理がある場合に必要となります。
iOSでは `GCD` という機構が用意されており、それを使って特定の処理のみをメインスレッドで処理することができます。
iOSでは、UI操作に関する処理はメインスレッドで処理することがルールとなっています。
メインスレッド以外でも処理はできますが、想定した動作とならないことが多いです。
（即座に反応しない、所定の位置に表示されない、そもそも表示されない、などなど）

## 最後に

いかがでしたでしょう？
Cocoapodsで導入したライブラリをReact Nativeから呼び出す、ということをやってみました。

Androidとコードが共通化できるReact NativeではiOSライブラリを利用することはあまりないかもしれませんが、
既存のiOSライブラリには便利なものがたくさん公開されています。
また、今回はSwiftで書きましたが、Swiftはまだまだバージョンアップが頻繁で古いコードではビルドが通らないこともよくあります。
ライブラリが追従しておらずビルドできないというケースも珍しくありません。
そのため注意が必要ではありますが、活用できる資産はどんどん使っていけばよいかなと私は思います。

もしかしたらですが、Objective-Cで書いたほうがビルドエラーに悩まされる事は少ないかもしれません。
（Swiftほど構文の変化はないはずなので）


Advent Calendarの投稿に遅れてしまいましたが、私の記事はこれで終わりです。