[React Native] 設定.appを開くNativeModuleを作った

ReactNativeで作ったアプリから、設定.appにジャンプしたい！ので、
Native Module を書きました。※ただしジャンプできるのはiOS8のみ

- [OpenSettingAppModule.h](https://github.com/betahikaru/ReactAwsApiViewer/blob/master/iOS/OpenSettingAppModule.h)
- [OpenSettingAppModule.m](https://github.com/betahikaru/ReactAwsApiViewer/blob/master/iOS/OpenSettingAppModule.m)


React Native はまだまだ発展途上なので、重箱の隅をつつきたいときは、Objective-Cのコード(Native Modules)を書く必要があります。
少なくとも実装した当時はまだReactNative公式では提供してなかったので、自分で作りました。

なお、設定.appの値を読み込むNative Modulesも作ったのですが、コミットした5日後に公式にもっとちゃんとしたNative Moduleが[追加](https://github.com/facebook/react-native/blob/5eca2e1d3cf42f40c648726f0ba7a2a621ab0127/Libraries/Settings/Settings.ios.js)されてました。。。

あと、NativeModuleでPromiseを返す機能も検討されているようです。楽しみです。
[[Bridge] Add support for JS async functions to RCT_EXPORT_METHOD](https://github.com/facebook/react-native/pull/1232)

----

以下は、Native Moduleを作った時の流れのメモです。

# 設定.appを開く(Objective-C)

iOS 5以前では開けたらしいのですが、iOS 6,7は開けず、iOS 8以降からは開けるようです。
今回、iOS 5以前は対象外としてます。

設定.appを開くには以下のようにします。

```objc
  NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
  BOOL result = [[UIApplication sharedApplication] openURL:url];
  if (result) { /* 成功 */ } else { /* 失敗 */ }
```

iOS8未満では UIApplicationOpenSettingsURLString を使おうとするとクラッシュ？するらしいので、以下のコードでiOSのバージョンを判定します。

```objc
  UIDevice *device = [UIDevice currentDevice];
  NSString *versionStr = [device systemVersion];
  NSString *versionSupportedOpening = @"8.0";
  NSComparisonResult resultCompare = [[device systemVersion]
                                       compare:versionSupportedOpening
                                       options:NSNumericSearch];
  if (resultCompare != NSOrderedDescending) {
    // iOS 8.0 未満
  } else {
    // iOS 8.0 以上
  }
```


# Objective-CのコードをNative Modulesとして書く
公式資料([Native Modules (iOS)](https://facebook.github.io/react-native/docs/nativemodulesios.html#content))を参考にブリッジを書きます。
資料を読めばだいたいわかります、が、iOSアプリを作り慣れてない自分は引っかかったので細かい補足をしていきます。

### .h, .mファイルの追加
モジュールを追加するには、まずXcodeで新しいファイルを追加します。
[iOS] - [Source] - [Cocoa Touch Class] を選びましょう。
言語にObjective-Cを選べば、いい感じに.hと.mファイルを生成してくれます。

ここではOpenSettingAppModuleという名前で作成します。

### RCTBridgeModuleプロトコル
Native Modlues は、RCTBridgeModuleプロトコルに従った実装が必要です。
具体的には以下の３点が必要になります。

- 親クラスを ```NSObject``` から ```NSObject <RCTBridgeModule>``` に変更。
- .m ファイル内で、```RCT_EXPORT_MODULE();```を呼び出す。
- .m ファイル内で、jsから呼び出すメソッドを定義する。
  - ```RCT_EXPORT_METHOD(メソッド名:引数...){実装}```

```objc:OpenSettingAppModule.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "RCTBridgeModule.h"
#import "RCTLog.h"

@interface OpenSettingAppModule : NSObject<RCTBridgeModule>

@end
```


```objc:CalendarManager.m
#import "OpenSettingAppModule.h"

@implementation OpenSettingAppModule

RCT_EXPORT_MODULE();

/**
 * Javascriptからの呼び出しは以下のとおり。
 *   OpenSettingAppModule.openSettingsApp(callback);
 */
RCT_EXPORT_METHOD(openSettingsApp:(RCTResponseSenderBlock)callback)
{
  /* Objective-Cで行いたい処理 */
}

@end
```

### RCT_EXPORT_METHOD() で定義できる引数の型
リテラルや関数、配列、マップが使えます。
詳しくは公式の[Argument Types](https://facebook.github.io/react-native/docs/nativemodulesios.html#argument-types)をみてください。


### 実装例
以上を踏まえて実装すると、以下のようになります。
ログを出力したいときは"RCTLog.h"をインポートして、RCTLogInfo()などで出力できます。

```objc:CalendarManager.m
RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(canOpenSettingsApp:(RCTResponseSenderBlock)callback)
{
  if (/* iOS8以上? */) {
    /* コールバックに渡す引数は、１つでも配列として渡す必要あり */
    callback(@[true]);
  } else {
    callback(@[false]);
  }
  RCTLogInfo(@"Finish");
}

RCT_EXPORT_METHOD(openSettingsApp:(RCTResponseSenderBlock)callback)
{
  /* 設定.appを開く処理 */
  if (/* 成功 */) {
    callback(@[@true]);
  } else {
    callback(@[@false]);
  }
  RCTLogInfo(@"Finish");
}

@end
```

# jsからNative Modulesを呼び出す
ここまでやって、やっとjsから処理を利用できます。

## require
まず、React Nativeのモジュール(NativeModules)をrequireします。

```js
var React = require('react-native');
var {
  ...,
  NativeModules,
} = React;
```

次に、今回追加したNativeModule(OpenSettingAppModule)をrequireします。

```js
var {
  OpenSettingAppModule,
} = NativeModules;
```

## 実行
NativeModule内で定義した```RCT_EXPORT_METHOD(){}```の処理を呼び出します。

```js
OpenSettingAppModule.openSettingsApp((result) => {
  /* 設定.appを開いた後の行いたい処理 */
});
```

古い生javascriptしか知らない自分でもわかるように書くと、以下のようになります。

```js
var callback = function(result) {
  /* 設定.appを開いた後の行いたい処理 */
}
OpenSettingAppModule.openSettingsApp(callback);
```

## 実装例
実際の実装は以下です。とんでもなく汚いです。

- [FetchedList.js](https://github.com/betahikaru/ReactAwsApiViewer/blob/master/jsx/component/FetchedList.js#L269-L273)