- 投稿日:2019-03-07T22:49:08+09:00
【SwiftChaining】イベントを送受信するプロトコル
SwiftChainingの解説記事その6です。
SwiftChainingとは何か?というのは、こちらの最初の記事を参考にしてください。
今までの記事では
SwiftChainingで用意されたクラスを使用して、バインディングする方法などを紹介してきました。これまでそれら用意されたクラスに関して「監視対象」とか「値を返せる・返せない」といった表現で説明をしていたのですが、実際のところは何のプロトコルに適合しているかという話になります。今回はそのプロトコルについて解説します。
プロトコル
クラスを
SwiftChainingでイベントの送受信などができるようにするために用意しているプロトコルがあります。以下の3つです。
- Sendable
- Fetchable
- Receivable
独自に作ったクラスでも、これらのプロトコルに適合させることで
SwiftChainingで同じように扱うことができます。Sendable
Sendableに適合したクラスは、監視対象となりイベントの送信ができます。定義は以下のような感じです。
public protocol Sendable: AnySendable { // 送信するイベントの型 associatedtype SendValue // chain()で返す型(あまり意識しなくて良い) typealias SenderChain = Chain<SendValue, SendValue, Self> } extension Sendable { // イベントを送信する(実装済。そのまま使う) public func broadcast(value: SendValue) { ... } // イベントの処理を構築する(実装済。そのまま使う) public func chain() -> SenderChain { ... } }実装例として、
Intを送ることに限定したNotifierを書いてみると、このようになります。class IntNotifier: Sendable { typealias SendValue = Int func notify(value: Int) { self.broadcast(value: value) } }イベントとして送る値を
SendValueで定義し、イベントを送信するときはbroadcast(value:)を呼びます。必要な実装はこれだけです。この
IntNotifierは、以下のように使うことができます。let notifier = IntNotifier() // chain()を呼べるので、イベントが送信された時に実行される処理を構築できる let observer = notifier.chain().do { print($0) }.end() // 値が送信されてdoが実行され、1がログに出力される notifier.notify(value: 1)Fetchable
Fetchableに適合したクラスは、Sendableのイベントを送信する機能に加え、sync()が呼ばれた時に値を引き出すことができます。定義は以下のようになっています。
public protocol Fetchable: Sendable { // 値を返す(要実装) func fetchedValue() -> SendValue // 値が返せるかをBoolで返す(必要に応じて実装) func canFetch() -> Bool } extension Fetchable { public func canFetch() -> Bool { // デフォルトではtrueを返す return true } }
Sendableで必要なSendValueに加えて、fetchedValue()を必ず実装する必要があります。返り値に返すのはsync()を呼んだ時に送信される値です。
fetchedValue()で値を引き出せないことがある場合にcanFetch()でfalseを返すとfetchedValue()は呼ばれません。デフォルトはtrueなので、必要があれば実装をしてください。こちらも実装例として、
Intを保持するIntHolderを書いてみるとこのようになります。class IntHolder: Fetchable { typealias SendValue = Int var value: Int = 0 { didSet { if self.value != oldValue { self.broadcast(value: self.value) } } } func fetchedValue() -> Int { return self.value } }以下のように使うことができます。
let holder = IntHolder() // sync()で初期値の0がログに出力される let observer = holder.chain().do { print($0) }.sync() // 値をセットすることでイベントが送信され、1がログに出力される holder.value = 1基本的には
broadcast(value:)で送る値とfetchedValue()で返す値は同じものになると思いますが、あえて変えるという使い方も考えられます。例えば、Viewを最初に表示するときはアニメーションしたくないけど、表示中のViewを更新するときはアニメーションさせたいなんていう場合には、
enumなどで値を送ってアニメーションの有無を変えるというのもありでしょう。Receivable
Receivableに適合したクラスは、sendTo()でイベントを受信することができます。定義は以下の通りです。
public protocol Receivable: class { // 受信するイベントの型 associatedtype ReceiveValue // 受信した時の処理(要実装) func receive(value: ReceiveValue) }受け取るイベントの型を
ReceiveValueで定義し、receive(value:)を実装する必要があります。イベントで送信されてきた値がvalueに入ってきますので、受け取った時の処理を書いてください。例として、
Fetchableの例のIntHolderをさらに拡張してみたいと思います。class IntHolder: Fetchable, Receivable { typealias SendValue = Int typealias ReceiveValue = Int var value: Int = 0 { didSet { if self.value != oldValue { self.broadcast(value: self.value) } } } func fetchedValue() -> Int { return self.value } func receive(value: Int) { self.value = value } }以下のように使うことができます。
let sender = IntHolder(0) let receiver = IntHolder(1) // syncで値が同期され、receiverは0になる let observer = sender.chain().sendTo(receiver).sync() // 0がログに出力される print(receiver.value) // イベントが送信され、receiverは2になる sender.value = 2 // 2がログに出力される print(receiver.value)
Receivableに関しては、SendableやFetchableのように重要な機能を有しているわけではなく、doを使えば可能なことをsendToで簡単にする程度のものになります。
- 投稿日:2019-03-07T20:29:27+09:00
【Swift4.2】EnumEnumerable が使えなくなっていた、、
Xcode10対応
Swift4.2が登場してからかなりお世話になっていたCaseIterable
これ自体はかなり助けられていたのだが、、
2019/03以降のXcode10対応が必須となって、保守していたアプリをXcode10に対応させる必要が生じた、、
Xcode10対応についてXcode9.4.1からXcode10.1でビルドしたが、、まさかのEnumEnumerableのCasesが取得できない、、という自体が発生。
影響範囲を抑えた修正をしたかったが、方法が見当たらなかったので、CaseIterableへの変更にて対応した
もし、なにか情報いただけましたら、教えてください。
Xcode v10.1 にはしたいけれど、 Swift v4.2 にはすぐにはできないというケースの場合
Xcode 10 から hashValue の扱いが変わっていて、紹介されている EnumEnumerable が動作しません。
stackoverflowの記事が参考になります。hashValue を使わないで実現している方法の紹介です。
参考資料
Swift4.0→4.2でEnumEnumerableが不要になった
変更履歴
2019/03/10 @AnzNetJp の助言により、EnumEnumerableのXcode10版について追記しました。
- 投稿日:2019-03-07T13:49:19+09:00
MultipeerConnectivityを使用した接続でserviceTypeに数値のみを設定するとクラッシュする
結論
設定できる条件は以下
- 1文字以上、15文字以内
- 使えるのは英数字とハイフン('a'-'z','A'-'Z','0'-'9','-')
- 少なくとも1つの文字を含まなければならない('a'-'z','A'-'Z')
- ハイフンで始まり or 終わりは不可
- 連続したハイフンは不可
なぜ?
RFCによって定められています
数字だけを使うとどうなる?
実行時にクラッシュします?
受信側.swiftlet peer = MCPeerID(displayName: "MyDisplayName") // 以下でクラッシュ let nearbyAdvertiser = MCNearbyServiceAdvertiser(peer: peer, discoveryInfo: nil, serviceType: "111111")送信側let peer = MCPeerID(displayName: "MyDisplayName") // 以下でクラッシュ let nearbyBrowser = MCNearbyServiceBrowser(peer: peer, serviceType: "11111")このように送信側、送信側どちらもクラッシュしてしまいます
最後に
serviceTypeを設定する際には気をつけましょう
- 投稿日:2019-03-07T13:19:22+09:00
Custom URL Schemeに使える文字列
URL Schemeには使える文字列が定められている
RFCによって定められています
scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
文字列で始まり、n個以上の文字,数字,ハイフン,ドット,プラスのみ※アンダースコアやスラッシュは使えません
ハイフンの代わりにアンダースコア(_)やスラッシュ(/)を使いがちですが、無効なスキームになってしまうので気をつけるように
ex...sample_app://,sample/app://などはNG
- 投稿日:2019-03-07T00:16:28+09:00
「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 のインストールとアプリのひな形の作成
では、さっそく元気に新プロジェクトつくってやっていきましょう。
$ yarn global add react-native-cli $ cd /path/to/somewhere $ react-native init So2Qiitaもし CamelCase なフォルダ名が気持ち悪ければ、ひな形作成後に、
$ mv SO2Qiita so2qiitaリネームしても大丈夫です。また、git にもコミットしておきます。
$ git init $ git add -A $ git ci -m "Create project via react native cli"ここで一度実行してみる。
初めて起動する場合や、metro という React Native 用に Javascript の Bundle を行うプロセスが起動していない場合は cli から、$ react-native run-iosで起動。
ひとたび React-Native の画面のレンダリングがうまく実行できるようになれば、Objective-C や Swift を変更したとき、 Xcode からビルドすることも可能といえば可能です。
しかし、簡便のため、本記事で「実行する」と書いた場合は、CLI で、react-native run-iosしていると考えておいてください。ひとたびビルドタスクがはじまると、マシンが熱をはらみ、CPU のほとんどを持っていかれるので、人間は大人しく wait 状態に入りましょう。
さらに、
App.jsxを編集して、Cmd+Rすると、画面が変わることも確認できたでしょうか。2. Xcode を開いて、Action Extension を追加。
まずは言葉の定義から。
App Exntesions |- Share Extension |- Action ExtensionApp Extensions は総称。具体的な実装として、Share Extension と Action Extension がある。他にもあるかもしれないが知らない。
Share Extension は現在閲覧中の情報を他のアプリや SNS・サービスで共有するための拡張で、ActionExtension は現在閲覧中の情報に関連したタスクを実行するための拡張です。
このあたりの仕分けはけして自明ではなく、URL をブックマークするにしてもはてブを使うなら、Share Extension っぽいし、Safari のブックマークに残すなら、Action Extension っぽい。
実装としてはほぼ同じものになるので、Apple のガイドラインを読んだ上で、より自分の作る拡張に適した方を選んでください。
また、これらの拡張機能の設計指針についても、ガイドラインでしっかり触れられているので、軽く目を通しておいて損はない。
煩雑な UI にするなとか、単一の機能を持つように設計しろとか、まぁそういった内容。今回は閲覧中の Stack Overflow のページからタグ情報を抜き出し、Qiita のページを開く、という動作で、Share じゃない感が強いし、Action Extension を選択します。
さて、Action Extension を作成すると決まったところで、いよいよ Xcode を開きます。
普段のお仕事がサーバーサイドだったら、Xcode を開くのは、年に数回あるかないかというところで、不慣れではありますが、怖がらないで、やっていきましょう。open ios/So2Qiita.xcodeproj/Xcode がおもむろにたちあがります。以下のメニューを選択して、
File > New > Target ...ウィザードから、
Action Extensionを選択。Swift で書きたいんだけど、ここはいったん Objective-C を選んでおいてください。
名前は、So2QiitaExtとした。Activateするか聞かれるので、当然Yes, "Activate"。
これで、空の
ActionExtensionができたことになる。React Native を使えるようにするために、
So2QiitaExtに依存関係として、JavaScriptCore.frameworkとlibRCTxxx.aが10個, それにlibReact.aというライブラリを追加します (tvOS 向けと間違いないようにしてください)。
あと、Qiita を WebView で開くので、libRNCWebView.aも追加しておいてください。結果、こんな感じになる。
また、
build phrasesの、Other Linker flagsに、-ObjC-lc++を追加します。謎めいた作業のように思われるかと思いますが、ここをきちんとしておかないと、後で詰むので、今がんばってください。
また、
Deployment Info>Deployment Targetが最新のものになっていると、Readt Native からrun-iosしたときの simulator から実行したときには deploy されないので、バージョンを合わせておきましょう。2019 年 3 月時点では、11.4にする必要がありました。ちなみに、asset 由来の color などが理由でビルドが失敗する場合、
.xcassetsファイルは削除しても問題ありません。今回のチュートリアルでは使用しないです。さて、ここでまた実行してみましょう。
実行後、Safari を起動して、Share ボタンをクリック。So2QiitaExtが無事に表示されていれば成功。これからは毎回書かないけれど、section 毎に、
git commitしておくと便利です。3. Action Extension から React Native を呼び出す。
Xcode での作業がもう少し続きます。
いま
So2QiitaExtを開いて実行すると、画像を受け取り、そのまま表示するというデフォルトの動作になっています。
(ぜひ、Photos アプリから写真を選択して試してみてください)Safari から実行しても画像が渡されないためなにも表示されず、空白のモーダルに、Done ボタンがあるだけです。
実はこの動作は、So2QiitaExt/ActionViewController.m内のviewDidLoadメソッドで定義されています。このメソッドをごっそり削除して、代わりに
// ActionViewController.m #import <React/RCTBundleURLProvider.h> #import <React/RCTRootView.h>をヘッダに追加してから、
// 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
などを参照のこと。
さて、これで実行し、Safari から Share ボタン経由で、
So2QiitaExtを立ち上げると空のモーダルが出現します。
ふむ、これはなにかおかしいですね。
本来なら、コンテナアプリの起動画面と同様に"Welcome to React Native"が出て欲しいところです。実はセキュリティ保護の観点からデフォルトでは
localhostも含めて http 通信はブロックされており、かつ React Native は開発環境下では Hot Reload やデバッグを有効にするために、http でjs.bundleをダウンロードして実行しているのです。ということで、
So2QiitaExtのInfo.plistに以下の項目を追加します。このXMLをコピペしてもいいよ。
<key>NSExceptionDomains</key> <dict> <key>localhost</key> <dict> <key>NSExceptionAllowsInsecureHTTPLoads</key> <true/> </dict> </dict>そして、再実行。
今回はウェルカムメッセージがモーダル内に表示されたのではないでしょうか。しかし、なんと、モーダルを消すすべがありません。
いまのところ、「上へスワイプ」から Safari を kill することでなんとか終了してください。4. Action Extension から呼び出されたとき用の画面を作る
モーダルを消す方法は少し先延ばしにして、ここからは Action Extension 用の画面を作っていきましょう。
方策としては、
initialPropertiesに Action Extension からのリクエストの場合のみ True になるフラグを設定して分岐する- エントリーポイントとなるファイルを
index.jsではないなにかに変えるが考えられます。正直、どちらでもかまわないのですが、今回は特にファイルを分けるほどの理由もないので、方策 1 を採用しましょう。
loadView メソッドに
// 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 側では、
// 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によって、表示するメッセージを変えてみます。実行。やりましたね。
5. モーダルを終了するためのボタンを作る
では、次に、モーダルを閉じるためのボタンを作りましょう。
今までは iOS からのアクションやメッセージを受け取るだけでしたが、今回はじめて React Native 側から、iOS へメッセージを送ることになります。
これを実現するためには、iOS 側でメッセージを受け取るためのインタフェースを用意する必要があります。せっかくですので、Objective-C ではなくて、Swift でそのブリッジ部分を作ってみましょう。
ここがたぶん、このチュートリアルの一番難しいところ。
みなさん、無事に乗り切ってください。まずは、Swift ファイルを
So2QiitaExtに追加します。New Fileから、Swift を選び、ActionExtensionという名前でファイルを作ります。ブリッジファイルを作るかと聞かれるので、
Create Bridging Headerを選んで、作成してください。まだファイルの中は変えなくていいです。
同名の Objective-C ファイル、ActionExtensionも作りましょう。こういったファイルが
So2QiitaExt以下に追加されているはずです。ここでブリッジ部分の実装を行いますが、Swift だけでは完結せず、Objective-C から、モジュールやメソッドを Extern 宣言、すなわち外部公開する必要があります。
ActionExtensionというクラスと、doneというメソッドを定義して、Javascript からアクセスできるようにしてみましょう。まずはブリッジヘッダに必要なヘッダを追加
// 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"外部公開用の宣言を追加
// ActionExtension.m #import "React/RCTBridgeModule.h" @interface RCT_EXTERN_MODULE(ActionExtension, NSObject) RCT_EXTERN_METHOD(done) @endそして、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メソッドを呼び出すコードを追加しましょう。// 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メソッドが呼ばれ、ログに表示されていますね。地味ですが大きな意味を持つ一歩です。
では、
doneの実装を行い、本当にモーダルを閉じることができるようにします。まずは、
ActionViewController.hでactionViewControllerインスタンスの外部公開と、doneメソッドの宣言を追加します。// ActionViewController.h #import <UIKit/UIKit.h> @interface ActionViewController : UIViewController extern ActionViewController * actionViewController; - (void) done; @end実装ファイルでは、
actionViewControllerに値をセット。doneメソッドは初めに作成したひな形にすでに実装されてあるので、このままこれを流用して Swift から呼ぶという算段です。// 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。
// ActionExtension.swift class ActionExtension: NSObject { @objc func done() { os_log("done", log: log, type: .default) actionViewController.done() } }はい、また、実行。
よかったね、無事モーダルが隠れました。
6. App Extensions から現在の URL を受け取る
ずいぶん長くなりましたが、このセクションが本機能の肝心な部分。Action Extension が起動された元のアプリから URL を JavaScript で受け取ります。
今回の場合、Safari から StackOverflow の Question の URL が受け取りたい情報ですね。まずは、JavaScript 側の実装のイメージ。関数の
callbackを使う場合。NativeModules.ActionExteion.url( (error, url) => if(!error) this.setState({ url }));
Promiseとして受け取る場合.NativeModules.ActionExteion.url() .then( (url) -> this.setState({ url })) .catch(e => console.log(e));どっちでもいいんだけど、好みで、
Promiseでやってみましょう。
asyncawaitを使ってモダンにやるぞ。まずは
urlというメソッドを定義し、Promiseを返すメソッドであるという宣言をブリッジ部分に書く。おまじないです。// ActionExtension.m RCT_EXTERN_METHOD(url: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject)そして、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 のドキュメントか、Medium で人気 TeaBreak の記事、Swift in React Native - The Ultimate Guide Part 1: Modulesが詳しいです。
// App.js async componentDidMount() { try { const url = await NativeModules.ActionExtension.url(); this.setState({ url }); catch(error) { console.log(error); } }また、実行。
やった、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 の実装となっていく)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というクラスを作って、ダミーの実装をする.export default class StackOverflow { constructor(url) { this.url = url; } getTags() { return Promise.resolve(["javascript", "reactjs"]); } }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で追加。yarn add react-native-webview react-native link react-native-webviewXcode のビルド設定を見て、
So2QiitaExtにlibRNCWebView.aが含まれているか確認しよう。
入ってないと、"Invariant Violation: requireNativeComponent: "RNCWebView" was not found" が襲いかかってくる。あと、全面 WebView だとせっかくつくった
doneメソッドを呼べなくなるので、NavigationBarも追加しておこう。yarn add react-native-navbar
stateにuriがあるときの View はこんな感じになる。<SafeAreaView style={styles.extension}> <NavigationBar title={{ title: "So2QiitaExt" }} leftButton={{ title: "Done", handler: this._onPress }} /> <WebView style={styles.webview} source={{ uri }} /> </SafeAreaView>うまくいけば、たぶん見慣れたサイトがモーダル上に表示される。
最後に、
StackOverflow.getTags()をちゃんとした実装にする。
StackAppsというところから、API Keyの登録をしなくちゃいけないと思っていたが、実はこのエンドポイントはpublicなようで、認証なしで呼べる。APIの詳細は、この辺に転がってます。
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]; }はい、これで完成
どうだ、このセクションでは、Js しか書いてないぞ!
Test だって、Jest で書けるぞ!ということで、React Native から、Action Extension を使う方法を紹介しました。
ごくろうさまでした。最終的な成果物は、
https://github.com/o3c9/so2qiita
ここで公開しています。
参考になったと思えば、ぜひ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 に完成したコードあるから、これcloneして動かしてみてください。
途中経過の再現については Xcode の設定や、コードをにらめっこしながら、差分を見つけてみましょう。
簡単じゃないけど、難しくもないはず。時間はかかるけど、こういうのがいい勉強になるよ。Xcode 上での作業が多い、もっとラクにできないの?
そう思ってた頃もあった。
React Native Share Extensionというものがあって、わざわざライブラリになっているにもかかわらず、README に書いてある Setup のプロセスがめっちゃ長くて大変そう。
つまり、ここに書いたくらいのステップがほぼ現在のところ最短だと思う。
一回やってみて要領つかめたら、次からは怖くないExtension のデバッグつらくない?
うん、つらい。
NativeModules.ActionExtension.xxxが不要な場合には、Extension の画面をコンテナアプリのトップ画面にして開発すると、JS 由来の動作確認がしやすくなる。Extension として実行しているときは、console.log の代わりに、自前の logger を作って、React Native の画面に出してしまうというのをやっていた。こんな感じ。
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 のStarの進捗次第かな






















