「StackOverflow から突然 Qiita に移動」を、App Extensions と React Native で簡単プロトタイプ

なんか実装でハマってしまい、うまくいかねーなって思いながら、StackOverflow を見ていたら、なんだか似たような質問 Qiita で見たし、やっぱり日本語で読みたいから Qiita に行こうかな、と思うことありませんか？
ありますよね！

それ、Action Extension でさくっと実装できます！
しかも、ほんの少しの Swift と Objective-C を書けば、あとは React Native で書けます。

ということでやってみました。

## 0. 本稿について

- 対象者: iOS プログラミング初心者. Xcode といえば、`xcode-select --install`だと思っている人
- 書いている人: iOS プログラミング初心者. Swift は`print("Hello, World")`だけで卒業した
- 必要な知識: JavaScript, React, React Native を触ったことがある
- 動作環境: macOS X mojave. React Native 0.58.x. Xcode や Swift のバージョンはしらない
- 内容: チュートリアル形式で Action Extension を React Native から使えるまでの実装方法を紹介する
- 初公開日: 2019/03/06 更新日は上に書いてあるよ

## 1. React Native CLI のインストールとアプリのひな形の作成

では、さっそく元気に新プロジェクトつくってやっていきましょう。

```console
$ yarn global add react-native-cli
$ cd /path/to/somewhere
$ react-native init So2Qiita
```

もし CamelCase なフォルダ名が気持ち悪ければ、ひな形作成後に、

```console
$ mv SO2Qiita so2qiita
```

リネームしても大丈夫です。また、git にもコミットしておきます。

```console
$ git init
$ git add -A
$ git ci -m "Create project via react native cli"
```

ここで一度実行してみる。
初めて起動する場合や、metro という React Native 用に Javascript の Bundle を行うプロセスが起動していない場合は cli から、

```console
$ react-native run-ios
```

で起動。

ひとたび React-Native の画面のレンダリングがうまく実行できるようになれば、Objective-C や Swift を変更したとき、 Xcode からビルドすることも可能といえば可能です。
しかし、簡便のため、本記事で「実行する」と書いた場合は、CLI で、`react-native run-ios`していると考えておいてください。

ひとたびビルドタスクがはじまると、マシンが熱をはらみ、CPU のほとんどを持っていかれるので、人間は大人しく wait 状態に入りましょう。

<img width="375" alt="Simulator Screen Shot - iPhone 7 - 2019-03-04 at 19.47.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/40c16a37-156d-ce31-cf2d-8a76cad82531.png">　<img width="375" alt="Simulator Screen Shot - iPhone X - 2019-03-04 at 19.54.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/5102e693-45de-95ad-992d-840b9aafb7ac.png">

さらに、`App.jsx`を編集して、`Cmd+R` すると、画面が変わることも確認できたでしょうか。

<img width="375" alt="Simulator Screen Shot - iPhone X - 2019-03-04 at 19.55.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/0f691c13-6c3d-c7dd-01e5-2283095ed1e5.png">


## 2. Xcode を開いて、Action Extension を追加。

まずは言葉の定義から。

```
App Exntesions
  |- Share Extension
  |- Action Extension
```

App Extensions は総称。具体的な実装として、Share Extension と Action Extension がある。他にもあるかもしれないが知らない。

Share Extension は現在閲覧中の情報を他のアプリや SNS・サービスで共有するための拡張で、ActionExtension は現在閲覧中の情報に関連したタスクを実行するための拡張です。

このあたりの仕分けはけして自明ではなく、URL をブックマークするにしてもはてブを使うなら、Share Extension っぽいし、Safari のブックマークに残すなら、Action Extension っぽい。

実装としてはほぼ同じものになるので、Apple の[ガイドライン](https://developer.apple.com/design/human-interface-guidelines/ios/extensions/sharing-and-actions/)を読んだ上で、より自分の作る拡張に適した方を選んでください。

また、これらの拡張機能の設計指針についても、[ガイドライン](https://developer.apple.com/design/human-interface-guidelines/ios/extensions/sharing-and-actions/)でしっかり触れられているので、軽く目を通しておいて損はない。
煩雑な UI にするなとか、単一の機能を持つように設計しろとか、まぁそういった内容。

今回は閲覧中の Stack Overflow のページからタグ情報を抜き出し、Qiita のページを開く、という動作で、Share じゃない感が強いし、Action Extension を選択します。

さて、Action Extension を作成すると決まったところで、いよいよ Xcode を開きます。
普段のお仕事がサーバーサイドだったら、Xcode を開くのは、年に数回あるかないかというところで、不慣れではありますが、怖がらないで、やっていきましょう。

```bash
open ios/So2Qiita.xcodeproj/
```

Xcode がおもむろにたちあがります。以下のメニューを選択して、

```
File > New > Target ...
```

<img width="700" alt="Screen Shot 2019-03-04 at 19.33.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/e0aa571b-aad4-923f-9095-2d6447be8062.png">

ウィザードから、`Action Extension`を選択。

<img width="727" alt="Screen Shot 2019-03-04 at 19.33.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/83bebf54-843e-8104-b863-f45817ba17eb.png">

Swift で書きたいんだけど、ここはいったん Objective-C を選んでおいてください。
名前は、`So2QiitaExt`とした。

<img width="723" alt="Screen Shot 2019-03-04 at 19.34.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/ec68c23d-8c5b-afec-a67c-ed794c8e31b4.png">

Activateするか聞かれるので、当然Yes, "Activate"。

<img width="419" alt="Screen Shot 2019-03-04 at 19.34.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/468f4342-f76e-703f-b6c4-9486cc2eb752.png">

これで、空の`ActionExtension`ができたことになる。

React Native を使えるようにするために、`So2QiitaExt`に依存関係として、`JavaScriptCore.framework` と `libRCTxxx.a`が10個, それに`libReact.a` というライブラリを追加します (tvOS 向けと間違いないようにしてください)。
あと、Qiita を WebView で開くので、`libRNCWebView.a`も追加しておいてください。

結果、こんな感じになる。

<img width="1059" alt="Screen Shot 2019-03-06 at 23.23.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/0162c051-d0bc-7ae8-d893-8487dfd95202.png">

また、`build phrases`の、`Other Linker flags` に、`-ObjC` `-lc++`を追加します。

<img width="451" alt="Screen Shot 2019-03-06 at 23.24.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/8637c3e8-32ca-7703-649b-cdec93dca784.png">

謎めいた作業のように思われるかと思いますが、ここをきちんとしておかないと、後で詰むので、今がんばってください。

また、`Deployment Info` > `Deployment Target` が最新のものになっていると、Readt Native から `run-ios` したときの simulator から実行したときには deploy されないので、バージョンを合わせておきましょう。2019 年 3 月時点では、`11.4` にする必要がありました。

<img width="654" alt="Screen Shot 2019-03-04 at 19.59.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/0ed546c1-3948-b338-51c1-67ac8aec8bd7.png">

ちなみに、asset 由来の color などが理由でビルドが失敗する場合、`.xcassets`ファイルは削除しても問題ありません。今回のチュートリアルでは使用しないです。

さて、ここでまた実行してみましょう。
実行後、Safari を起動して、Share ボタンをクリック。`So2QiitaExt` が無事に表示されていれば成功。

<img width="375" alt="Simulator Screen Shot - iPhone 7 - 2019-03-04 at 20.00.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/efbfe1c5-31f7-a024-055c-700787322ffa.png">
<img width="375" alt="Simulator Screen Shot - iPhone 7 - 2019-03-04 at 20.00.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/558f826b-d5e7-281d-e07c-c7fec21416a7.png">

これからは毎回書かないけれど、section 毎に、`git commit`しておくと便利です。

## 3. Action Extension から React Native を呼び出す。

Xcode での作業がもう少し続きます。

いま`So2QiitaExt`を開いて実行すると、画像を受け取り、そのまま表示するというデフォルトの動作になっています。
（ぜひ、Photos アプリから写真を選択して試してみてください）

Safari から実行しても画像が渡されないためなにも表示されず、空白のモーダルに、Done ボタンがあるだけです。
実はこの動作は、`So2QiitaExt/ActionViewController.m`内の`viewDidLoad`メソッドで定義されています。

このメソッドをごっそり削除して、代わりに

```objc
//  ActionViewController.m

#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
```

をヘッダに追加してから、

```objc
//  ActionViewController.m

- (void)loadView {
  NSURL *jsCodeLocation;

  jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];

  RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                      moduleName:@"So2Qiita"
                                               initialProperties:nil
                                                   launchOptions:nil];
  rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
  self.view = rootView;
}
```

をというメソッドを追加してください。
`viewDidLoad`と`loadView`はなにが違うのか、気になる方は、

[UIViewController のライフサイクル - Qiita](https://qiita.com/motokiee/items/0ca628b4cc74c8c5599d)

などを参照のこと。

さて、これで実行し、Safari から Share ボタン経由で、`So2QiitaExt`を立ち上げると空のモーダルが出現します。
ふむ、これはなにかおかしいですね。
本来なら、コンテナアプリの起動画面と同様に"Welcome to React Native"が出て欲しいところです。

実はセキュリティ保護の観点からデフォルトでは `localhost` も含めて http 通信はブロックされており、かつ React Native は開発環境下では Hot Reload やデバッグを有効にするために、http で `js.bundle` をダウンロードして実行しているのです。

ということで、`So2QiitaExt`の `Info.plist` に以下の項目を追加します。

<img width="407" alt="Screen Shot 2019-03-06 at 23.30.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/58c3e04e-8f64-a3a0-32ff-fe071d1d92bb.png">

このXMLをコピペしてもいいよ。

```xml
<key>NSExceptionDomains</key>
<dict>
  <key>localhost</key>
  <dict>
    <key>NSExceptionAllowsInsecureHTTPLoads</key>
    <true/>
  </dict>
</dict>
```

そして、再実行。
今回はウェルカムメッセージがモーダル内に表示されたのではないでしょうか。

<img width="375" alt="Simulator Screen Shot - iPhone X - 2019-03-04 at 21.53.40.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/b7c28501-9c5d-8f2a-9318-ca6bc9786454.png">

しかし、なんと、モーダルを消すすべがありません。
いまのところ、「上へスワイプ」から Safari を kill することでなんとか終了してください。

## 4. Action Extension から呼び出されたとき用の画面を作る

モーダルを消す方法は少し先延ばしにして、ここからは Action Extension 用の画面を作っていきましょう。
方策としては、

1. `initialProperties`に Action Extension からのリクエストの場合のみ True になるフラグを設定して分岐する
2. エントリーポイントとなるファイルを`index.js`ではないなにかに変える

が考えられます。正直、どちらでもかまわないのですが、今回は特にファイルを分けるほどの理由もないので、方策 1 を採用しましょう。

loadView メソッドに

```objc
//  ActionViewController.m

NSDictionary *initialProps = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool: TRUE] forKey:@"isExtension"];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                    moduleName:@"So2Qiita"
                                             initialProperties:initialProps
                                                 launchOptions:nil];
```

を追加。
`RCTRootView` のイニシャライザに、 `initialProperties` として、`{isExtension: true}`を渡すようにします。

そして、React Native 側では、

```jsx
// App.js

export default class App extends Component<Props> {
  render() {
    const { isExtension } = this.props;
    let message;
    if (isExtension) {
      message = "Welcome to So2QiitaExt on React Native!";
    } else {
      message = "Welcome to So2Qiita on React Native!";
    }

    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>{message}</Text>
      </View>
    );
  }
}
```

`isExtension`によって、表示するメッセージを変えてみます。実行。

<img width="375" alt="Simulator Screen Shot - iPhone X - 2019-03-04 at 22.07.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/d6c5e986-fc39-922b-d081-346e5ef59ce8.png">

やりましたね。

## 5. モーダルを終了するためのボタンを作る

では、次に、モーダルを閉じるためのボタンを作りましょう。
今までは iOS からのアクションやメッセージを受け取るだけでしたが、今回はじめて React Native 側から、iOS へメッセージを送ることになります。
これを実現するためには、iOS 側でメッセージを受け取るためのインタフェースを用意する必要があります。

せっかくですので、Objective-C ではなくて、Swift でそのブリッジ部分を作ってみましょう。
ここがたぶん、このチュートリアルの一番難しいところ。
みなさん、無事に乗り切ってください。

まずは、Swift ファイルを`So2QiitaExt`に追加します。`New File`から、Swift を選び、`ActionExtension`という名前でファイルを作ります。

<img width="838" alt="Screen Shot 2019-03-04 at 22.45.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/17d2fc23-d6b9-3f95-c843-d07ed5b1f43f.png">

ブリッジファイルを作るかと聞かれるので、`Create Bridging Header` を選んで、作成してください。

<img width="560" alt="Screen Shot 2019-03-04 at 22.45.32.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/559a0a21-8c84-1f72-5b9e-783e177adcfd.png">

まだファイルの中は変えなくていいです。
同名の Objective-C ファイル、`ActionExtension`も作りましょう。

こういったファイルが`So2QiitaExt`以下に追加されているはずです。

<img width="240" alt="Screen Shot 2019-03-04 at 22.57.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/e4f115bd-830f-f225-5f92-a4ac30e58f53.png">

ここでブリッジ部分の実装を行いますが、Swift だけでは完結せず、Objective-C から、モジュールやメソッドを Extern 宣言、すなわち外部公開する必要があります。

`ActionExtension`というクラスと、`done`というメソッドを定義して、Javascript からアクセスできるようにしてみましょう。

まずはブリッジヘッダに必要なヘッダを追加

```objc
// So2QiitaExt-Bridging-Header.h
//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//
#import "React/RCTBridgeModule.h"
#import "ActionViewController.h"
```

外部公開用の宣言を追加

```objc
//  ActionExtension.m
#import "React/RCTBridgeModule.h"

@interface RCT_EXTERN_MODULE(ActionExtension, NSObject)

RCT_EXTERN_METHOD(done)

@end
```

そして、Swiftで実装

```swift
// ActionExtension.swift

import Foundation
import os.log

let log = OSLog(subsystem: "com.o3c9.so2qiita", category: "ActionExtension")

@objc(ActionExtension)
class ActionExtension: NSObject {
  @objc
  func done() {
    os_log("done", log: log, type: .default)
  }
}
```

`os_log`を仕込むと、`Console.app`にログを吐き出すことができるようになります。
これが今後君の命綱となる。

そして、`App.js`に、`done`メソッドを呼び出すコードを追加しましょう。

```jsx
// App.js

export default class App extends Component {
  _onPress() {
    NativeModules.ActionExtension.done();
  }

  render() {
    ...

    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>{message}</Text>
        {isExtension && <Button onPress={this._onPress} title="Done" />}
      </View>
    );
  }
}
```

そんでもって実行〜。

Donwをタップすると、無事にSwiftのdoneメソッドが呼ばれ、ログに表示されていますね。地味ですが大きな意味を持つ一歩です。

<img width="528" alt="Screen Shot 2019-03-05 at 21.35.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/e47f776c-f988-8e39-fa9e-caebade9405a.png">

では、`done`の実装を行い、本当にモーダルを閉じることができるようにします。

まずは、`ActionViewController.h`で`actionViewController`インスタンスの外部公開と、`done`メソッドの宣言を追加します。

```objc
// ActionViewController.h

#import <UIKit/UIKit.h>

@interface ActionViewController : UIViewController

extern ActionViewController * actionViewController;

- (void) done;

@end
```

実装ファイルでは、`actionViewController`に値をセット。`done` メソッドは初めに作成したひな形にすでに実装されてあるので、このままこれを流用して Swift から呼ぶという算段です。

```objc
//  ActionViewController.m

ActionViewController * actionViewController = nil;

@implementation ActionViewController

- (void)loadView {
  ...

  rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:0.0f blue:0.0f alpha:0.3];
  self.view = rootView;
  actionViewController = self;
}
```

で、ActionExtension.swift。

```swift
// ActionExtension.swift

class ActionExtension: NSObject {
  @objc
  func done() {
    os_log("done", log: log, type: .default)
    actionViewController.done()
  }
}
```

はい、また、実行。

![qiita_action.gif](https://qiita-image-store.s3.amazonaws.com/0/225534/030ac755-adea-369b-d5c3-c92baa2fdc7a.gif)

よかったね、無事モーダルが隠れました。

## 6. App Extensions から現在の URL を受け取る

ずいぶん長くなりましたが、このセクションが本機能の肝心な部分。Action Extension が起動された元のアプリから URL を JavaScript で受け取ります。
今回の場合、Safari から StackOverflow の Question の URL が受け取りたい情報ですね。

まずは、JavaScript 側の実装のイメージ。関数の`callback`を使う場合。

```jsx
NativeModules.ActionExteion.url( (error, url) => if(!error) this.setState({ url }));
```

`Promise`として受け取る場合.

```jsx
NativeModules.ActionExteion.url()
  .then( (url) -> this.setState({ url }))
  .catch(e => console.log(e));
```

どっちでもいいんだけど、好みで、`Promise` でやってみましょう。
`async` `await`を使ってモダンにやるぞ。

まずは`url`というメソッドを定義し、`Promise` を返すメソッドであるという宣言をブリッジ部分に書く。おまじないです。

```objc
// ActionExtension.m

RCT_EXTERN_METHOD(url: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject)
```

そして、Swift ファイルでの実装。

```swift
// ActionExtension.swift

@objc(ActionExtension)
class ActionExtension: NSObject {
  ...

  @objc
  func url(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) -> Void {
    guard
      let inputItem = actionViewController.extensionContext?.inputItems.first as? NSExtensionItem,
      let attachments = inputItem.attachments
      else {
        let error = NSError(domain: "", code: 400, userInfo: nil)
        reject("E_URL", "cannot obtain url", error)
        return
    }

    for provider in attachments {
      if provider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
        provider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil, completionHandler: { (target, error) in
          let url = target as! URL
          resolve(url.absoluteString)
        })
        break
      }
    }
  }
}
```

まぁ、こういうの、正直すべてを正しく理解するのにはたくさん寄り道をせなあかんので、全部は説明しきれないです。
ただ Swift や iOS プログラミングに関心があれば、ぜひ Apple の公式ドキュメントや有志の解説記事を探して読んでみてください。

React Native とのつなぎこみに関しては、[React Native の Official のドキュメント](https://facebook.github.io/react-native/docs/native-modules-ios)か、Medium で人気 TeaBreak の記事、[Swift in React Native - The Ultimate Guide Part 1: Modules](https://teabreak.e-spres-oh.com/swift-in-react-native-the-ultimate-guide-part-1-modules-9bb8d054db03)が詳しいです。

```jsx
// App.js

async componentDidMount() {
  try {
    const url = await NativeModules.ActionExtension.url();
    this.setState({ url });
  catch(error) {
    console.log(error);
  }
}
```

また、実行。

<img width="375" alt="Simulator Screen Shot - iPhone X - 2019-03-05 at 22.39.11.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/fabfd0d6-e8d6-50f0-5f75-49accae45c48.png">

やった、URLが表示されてますね。これで、Action Extension から URL を React Native の JavaScript で受け取ることができるようになりました。
めでたし、めでたし。

Action Extension を React Native と組み合わせて使う、という書きたかった内容はここまででほとんどすべて。

- `done`メソッドで、JS から Swift 側へメッセージを送る。
- `url`メソッドで、JS から Swift 側へデータを要求し受け取る。

この 2 つができるようになったわけです。

あとは、機能を完成させるために React Native 側での実装をさくっと紹介して終わることにします。

## 7. StackOverflow のタグを使って Qiita で検索する JavaScript

Safari から送られてきた URL を使って、StackOverflow のタグを Qiita 検索に使うというロジックの実装に入ります。

まずは、Component での実装のイメージ。
（補足すると、今の時点ではあくまでイメージだったはずが、この後、このイメージに沿って実装が進むため、結局これがそのまま Component の実装となっていく）

```jsx
async componentDidMount() {
  try {
    // URLをNativeModules経由で、上記のActionExtensionクラスから受け取る & Wait
    const url = await NativeModules.ActionExtension.url();
    // そのURLをStackOverflow APIを実装したクラスに渡して、タグを受け取る & Wait
    const tags = await new StackOverflow(url).getTags();
    // タグからQiitaの検索クエリを構築
    const query = encodeURIComponent(tags.map(t => `tag:${t}`).join(" "));
    const uri = `https://qiita.com/search?utf8=%E2%9C%93&q=${query}`;
    // stateにつっこむ
    this.setState({ isLoading: false, uri });
  } catch (error) {
    this.setState({ isLoading: false, error });
  }
}
```

`StackOverflow.js`というクラスを作って、ダミーの実装をする.

```jsx
export default class StackOverflow {
  constructor(url) {
    this.url = url;
  }

  getTags() {
    return Promise.resolve(["javascript", "reactjs"]);
  }
}
```

```jsx
class Extension extends Component {
  render() {
    if (isLoading) {
      return (
        <SafeAreaView style={styles.container}>
          <ActivityIndicator size="large" color="#0000ff" />
        </SafeAreaView>
      );
    } else if (uri) {
      return (
        <SafeAreaView style={styles.extension}>
          <Text>{uri}</Text>
        </SafeAreaView>
      );
    } else {
      return (
        <SafeAreaView style={styles.container}>
          <Text style={styles.error}>{error}</Text>
        </SafeAreaView>
      );
    }
  }
}
```

次に、Qiitaの検索結果ページを、WebView で表示させてみる。
`WebView`はReact Native本体から分離されて別パッケージになったようなので、`yarn`で追加。

```bash
yarn add react-native-webview
react-native link react-native-webview
```

Xcode のビルド設定を見て、`So2QiitaExt`に`libRNCWebView.a`が含まれているか確認しよう。
入ってないと、"Invariant Violation: requireNativeComponent: "RNCWebView" was not found" が襲いかかってくる。

あと、全面 WebView だとせっかくつくった`done`メソッドを呼べなくなるので、`NavigationBar`も追加しておこう。

```bash
yarn add react-native-navbar
```

`state`に`uri`があるときの View はこんな感じになる。

```jsx
<SafeAreaView style={styles.extension}>
  <NavigationBar
    title={{ title: "So2QiitaExt" }}
    leftButton={{ title: "Done", handler: this._onPress }}
  />
  <WebView style={styles.webview} source={{ uri }} />
</SafeAreaView>
```

うまくいけば、たぶん見慣れたサイトがモーダル上に表示される。

<img width="375" alt="Simulator Screen Shot - iPhone X - 2019-03-06 at 23.44.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/225534/bfa4877c-af4d-7ec9-5353-e2b58e3562ed.png">

最後に、`StackOverflow.getTags()`をちゃんとした実装にする。
StackAppsというところから、API Keyの登録をしなくちゃいけないと思っていたが、実はこのエンドポイントはpublicなようで、認証なしで呼べる。

APIの詳細は、[この辺](https://api.stackexchange.com/docs/questions-by-ids#order=desc&sort=activity&ids=50311473&filter=default&site=stackoverflow&run=true)に転がってます。

```jsx
constructor(url) {
  this.questionId = this._parseUrl(url);
}

getTags() {
  return new Promise(async (resolve, reject) => {
    if (this.questionId) {
      try {
        const response = await fetch(
          `https://api.stackexchange.com/2.2/questions/${this.questionId}?site=stackoverflow`
        );
        if (response.ok) {
          const json = await response.json();
          const tags = json.items[0].tags;
          return resolve(tags);
        } else {
          return reject(`http error: ${response.status}`);
        }
      } catch (error) {
        return reject(error.message);
      }
    } else {
      return reject("not a valid SO url");
    }
  });
}

URL_REGEX = /^https\:\/\/stackoverflow\.com\/questions\/(\d+)/;

_parseUrl(url) {
  const result = url.match(this.URL_REGEX);
  return result && result[1];
}
```

はい、これで完成 :sparkles:

![so2qiita.gif](https://qiita-image-store.s3.amazonaws.com/0/225534/343ec316-aec1-34ca-236e-b6e6b0fa73ec.gif)

どうだ、このセクションでは、Js しか書いてないぞ！
Test だって、Jest で書けるぞ！

ということで、React Native から、Action Extension を使う方法を紹介しました。
ごくろうさまでした。

最終的な成果物は、

[https://github.com/o3c9/so2qiita](https://github.com/o3c9/so2qiita)

ここで公開しています。
参考になったと思えば、ぜひStar :star: をよろしくです！


## 8. FAQ -結びにかえて

##### それ、全部 Swift で書いた方が早くない？

ほとんどの場合そうかもしれないが、コンテナアプリが React Native で書かれている場合、App Extensions でもその資産を使いたいことはあるはず。そういった場合には、なるべくすべて React Native にしておくほうがいいはずなので、ここで紹介したテクニックは使えると思う。

##### この機能、微妙じゃね？

タグ検索だけだと微妙だけど、タイトルから重要なキーワードを推測して適切な検索クエリを構築できるようになると、けっこう実用的だと思っている。それなりに自然言語処理がんばってやらないと使い物にならないだろうけどね。

##### エンジニアは全員英語で読み書きできるべきだし、日本語のQiita見るより英語のStack Overflowを参照するべきでは？

You `Qiita2So` 作るべき

##### Share Extension の例もほしい

表示されるカラムなどが違い、UI としては別物に見えるけど、実質ほぼ同じものなので、Action Extension が作れたら、Share Extension も作ることができる \#はず \#未確認 \#誰かやってみて \#コメント欲しい

##### チュートリアル通りやっても動かない

うん、それが現実。

[https://github.com/o3c9/so2qiita](https://github.com/o3c9/so2qiita) に完成したコードあるから、これcloneして動かしてみてください。

途中経過の再現については Xcode の設定や、コードをにらめっこしながら、差分を見つけてみましょう。
簡単じゃないけど、難しくもないはず。時間はかかるけど、こういうのがいい勉強になるよ。

##### Xcode 上での作業が多い、もっとラクにできないの？

そう思ってた頃もあった。

[React Native Share Extension](https://github.com/alinz/react-native-share-extension)というものがあって、わざわざライブラリになっているにもかかわらず、README に書いてある Setup のプロセスがめっちゃ長くて大変そう。
つまり、ここに書いたくらいのステップがほぼ現在のところ最短だと思う。
一回やってみて要領つかめたら、次からは怖くない

##### Extension のデバッグつらくない？

うん、つらい。

`NativeModules.ActionExtension.xxx`が不要な場合には、Extension の画面をコンテナアプリのトップ画面にして開発すると、JS 由来の動作確認がしやすくなる。

Extension として実行しているときは、console.log の代わりに、自前の logger を作って、React Native の画面に出してしまうというのをやっていた。こんな感じ。

```jsx
debug(message) {
  this.setState( { console: [...this.state.console, message] })
}

render() {
  return (
    <View>
      ...
      {this.state.console.map(m => <Text>{m}</Text>)}
    </View>
  )
}
```

iOS 側は、`os_log`を使えば、Console.app から見えるようになる。
他の大量のメッセージにかき消されるという苦難はあるけれど。
Process を限定してみれば、いちおう追えないことはない。

##### Android は？

また今度。
[https://github.com/o3c9/so2qiita](https://github.com/o3c9/so2qiita) のStarの進捗次第かな