swiftに、react ntaiveを組み込みたかったのだが、情報が少ない。
そんな折良さげなチュートリアルを見つけたので、紹介する。
react native内で、swiftのnative componentを扱う方法は今回は触れない。
まず以下をクリックして、starter projectをunzipしてください。
https://koenig-media.raywenderlich.com/uploads/2016/09/Mixer-Starter-3.zip
swiftで書かれた、簡素なアプリです。
途中で詰まったらチュートリアル終了後のコードを読んでみてください。
セットアップ
ダウンロードしたstarterプロジェクトのjsフォルダに以下のpackage.jsonファイルを作成
{
"name": "mixer",
"version": "1.0.0",
"private": true,
"description": "Mixer",
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start"
},
"dependencies": {
"react": "~15.3.1",
"react-native": "~0.34.0"
}
}
npm install
を実行。
次に、ios/Podfileを作成。
npm installした時に作成されるnode_modulesの中からReactをインストールする。
# podfile
use_frameworks!
target 'Mixer'
pod 'React', :path => '../js/node_modules/react-native', :subspecs => [
'Core',
'RCTImage',
'RCTNetwork',
'RCTText',
'RCTWebSocket',
]
pod install
を実行
cocoapodsを使用したので、Mixer.xcworkspaceという白いファイルが作成されました。
このファイルをxcodeで開きrunしてみましょう。青いファイルを選択すると動かないので注意してください。
以下の画面が現れることを確認してください。
npmとcocoapod の依存関係を無事にインストールできました。
ReactをIOSに埋め込む
次に、index.ios.jsというファイルをjsフォルダに作成します。
'use strict';
// 1
import React from 'react';
import ReactNative, {
AppRegistry,
StyleSheet,
Text,
View,
} from 'react-native';
// 2
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'green',
},
welcome: {
fontSize: 20,
color: 'white',
},
});
// 3
class AddRatingApp extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>We're live from React Native!!!</Text>
</View>
)
}
}
// 4
AppRegistry.registerComponent('AddRatingApp', () => AddRatingApp);
よくあるReact NativeのViewですね。
ArrRegistry.registerComponentで、swiftに渡すreactコンポーネンツを指定しています。
swiftに渡したいコンポーネントの数だけAppRegistry.requireComponentを書いてください。
次に、xcodeで、AddRatingViewController.swiftを開き、Reactをインポートします。
import React
次に、以下のインスタンス変数をvar mixer: Mixer!の下に加えます。
要するに、型を定義しています。
var addRatingView: RCTRootView!
そして、次に以下をviewDidLoad()の最後尾に加えます。
addRatingView = RCTRootView(
bundleURL: URL(string: "http://localhost:8081/index.ios.bundle?platform=ios"),
moduleName: "AddRatingApp",
initialProperties: nil,
launchOptions: nil)
self.view.addSubview(addRatingView)
bundleURLはreactコンポーネントを受け取りに行くURLを指定
moduleNameは、先ほどAppRegistryで登録したコンポーネントの名前を入れてください。
initialPropertiesは初期のpropsです
lauchOptions
最後に、viewDidLayoutSubviewsの中で、swiftに対してreactコンポーネントのフレームを追加します。
addRatingView.frame = self.view.bounds
ただ、このままでは動きません。
Appleはセキュリティ上の理由からIOSのURLへのHTTPアクセスを不許可にしてあります。
開発に必要なのでlocalhostへのhttpアクセスは許可しましょう。
xcodeからInfo.plistを開いて、次のように編集してください。
1. NSAppTransportSecurityをDictionaryタイプのキーとして作成
2. NSExceptionDomainsをDictionaryタイプのキーとして、NSAppTransportSecurityの下に作成
3. localhostというキーをDictionaryタイプとしてNSExceptionDomainsの下に作成
4. NSTemporaryExceptionAllowsInsecureHTTPLoadsをキーboolean型で、YESをvalueとしてlocalhostの下に作成
以上の操作を終えると、Info.plistは以下のようになります。
アプリをrunしてみよう
上のような画面がターミナル上に表示されます。
xcodeからrunしましょう。
AddRatingをタップすると以下のような緑の色の画面が表示されれば成功です。
複数コンポーネントのブリッジ
以下のスレッドとキューはコードの実行を管理する。
- Main Thread: このスレッドはUIKitによるnative viewの描画を扱う。
- Shadow Queue: このGCDキューはnative viewのレイアウトを計算する。
- javascript Queue: このキューはjsの実行を扱う
- Modules Queue: 通常ではそれぞれのカスタムnative moduleは個々のGCDキューを得る。
さっきRCTRootView(_:moduleName:initialProperties:launchOptions)を使いました。
この方法は、一箇所だけ、react componentを埋め込むには問題ありません。しかし、通常は、複数箇所に、reactを埋め込みたいはずです。(そうしないと、react nativeで開発ではなく、swiftでの開発になってしまいますからね。)
複数箇所にreactを埋め込みたい場合はまず、RCTBridgeインスタンスを作りましょう。
先ほどはRCTRootViewを使った。しかし、複数のReact Native viewをブリッジしたい場合、RCTBridgeを代わりに使う必要がある。
MixerReactModule.swift
を作成し、以下のように記述する
import Foundation
import React
class MixerReactModule: NSObject {
static let sharedInstance = MixerReactModule()
}
singletonパターンを使っている。(私はここら辺よく理解していない。)
以下の変数をclass内に加える。
var bridge: RCTBridge?
その後、RCTBridgeDelegateメソッドをファイルの後ろに記述する。
extension MixerReactModule: RCTBridgeDelegate {
func sourceURL(for bridge: RCTBridge!) -> URL! {
return URL(string: "http://localhost:8081/index.ios.bundle?platform=ios")
}
}
次にクラス内に、以下を記述
func createBridgeIfNeeded() -> RCTBridge {
if bridge == nil {
bridge = RCTBridge.init(delegate: self, launchOptions: nil)
}
return bridge!
}
func viewForModule(_ moduleName: String, initialProperties: [String : Any]?) -> RCTRootView {
let viewBridge = createBridgeIfNeeded()
let rootView: RCTRootView = RCTRootView(
bridge: viewBridge,
moduleName: moduleName,
initialProperties: initialProperties)
return rootView
}
viewForModule()は、RCTBridgeインスタンスが存在しない場合、RCTBridgeインスタンスを作成するためにcreateBridgeIfNeeded()を呼ぶ
RCTRootViewはRCTRootViewを
さあ、addRatingViewController.swift を開き、addRativgViewを
以下のように書き換えよう。
addRatingView = MixerReactModule.sharedInstance.viewForModule(
"AddRatingApp",
initialProperties: nil)
swiftでは、MixerReactModuleクラスがどのファイルからも自動で使えるのだろうか?
sharedInstanceとかの説明は本家チュートリアルに書いてあるが面倒なのでまだ訳していない。
xcodeで、runして、以下のような画面が表示されていたら成功だ。
見た目的には変化はないが、これで複数のreact コンポーネントを扱えるようになった。
index.ios.jsをリファクタリング
index.ios.jsにViewを記述するのは、保守性を低下させます。
別ファイルに書き出しましょう。
AddRatingApp.jsをjsディレクトリに作成する。
'use strict';
import React from 'react';
import ReactNative, {
StyleSheet,
Text,
View,
} from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'green',
},
welcome: {
fontSize: 20,
color: 'white',
},
});
class AddRatingApp extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>We're live from React Native!!</Text>
</View>
)
}
}
module.exports = AddRatingApp;
さて、このコードですが、idnex.ios.jsと酷似してますね。
なぜなら、ただ、index.ios.jsの中身を移しただけだからです。
index.ios.jsを以下のように変更しましょう。
リファクタリングできましたね。
'use strict';
import {AppRegistry} from 'react-native';
const AddRatingApp = require('./AddRatingApp');
AppRegistry.registerComponent('AddRatingApp', () => AddRatingApp);
Cmd + Rを押して、緑の画面が表示されれば成功です。
画面遷移を追加
AddRatingApp.jsを開きましょう。
そして、NavigatorとTouchableOpacityをインストールします。
...
Navigator,
TouchableOpacity,
} from 'react-native'
styleは以下のように書き換えましょう。
const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'green',
},
welcome: {
fontSize: 20,
color: 'white',
},
navBar: {
backgroundColor: '#25507b',
},
navBarText: {
fontSize: 16,
marginVertical: 10,
},
navBarTitleText: {
color: 'white',
fontWeight: '500',
marginVertical: 9,
},
navBarLeftButton: {
paddingLeft: 10,
},
navBarRightButton: {
paddingRight: 10,
},
navBarButtonText: {
color: 'white',
},
})
AddRatingAppクラスの中に画面遷移先の画面のコードを追加します。
_renderScene(route, navigator) {
return (
<View style={styles.content}>
<Text style={styles.welcome}>We're live from React Native!!!</Text>
</View>
);
}
ナビゲーションバーのタイトルのコンポーネントを追加します。
jsx
_renderNavTitle(route, navigator, index, navState) {
return <Text style={styles.navBarTitleText}>{route.title}</Text>;
}
次にクラス内にヘッダーの左のボタンのコンポーネントを追加します。
_renderNavLeftItem(route, navigator, index, navState) {
return (
<TouchableOpacity
onPress={() => console.log('Cancel button pressed')}
style={styles.navBarLeftButton}>
<Text style={[styles.navBarText, styles.navBarButtonText]}>
Cancel
</Text>
</TouchableOpacity>
);
}
次にクラス内にヘッダーの右のボタンのコンポーネントを追加します。
_renderNavRightItem(route, navigator, index, navState) {
return (
<TouchableOpacity
onPress={() => console.log('Save button pressed')}
style={styles.navBarRightButton}>
<Text style={[styles.navBarText, styles.navBarButtonText]}>
Save
</Text>
</TouchableOpacity>
);
}
最後に画面遷移を行うために、renderの中身を書き換えます。
render() {
return (
<Navigator
debugOverlay={false}
style={styles.container}
initialRoute={{title: 'Add Rating'}}
renderScene={this._renderScene.bind(this)}
navigationBar={
<Navigator.NavigationBar
routeMapper={{
LeftButton: this._renderNavLeftItem.bind(this),
RightButton: this._renderNavRightItem.bind(this),
Title: this._renderNavTitle.bind(this),
}}
style={styles.navBar}
/>
}
/>
);
}
initialRouteプロパティは、初期画面を表します。
シュミレーターをリロードしましょう。ヘッダーが現れたのを確認できましたか?!
mixer-add-navigation-1.png
左のボタンを押してください。以下のようなログがxcodeに現れます。
2016-09-21 17:20:13.085 [info][tid:com.facebook.react.JavaScript] Cancel button pressed
右のボタンを押すと以下が出力されます。
2016-09-21 17:20:27.838 [info][tid:com.facebook.react.JavaScript] Save button pressed
react native viewとコミュニケーションする
RCTRootViewを作成する時、initialPropertiesパラメータにデータを渡すことが出来る。データはコンポーネントのinitial propsとして渡され、もちろん後で、appPropertiesを渡すことで、このpropsをアップデートすることもできる。
// AddRatingViewController.swift
addRatingView = MixerReactModule.sharedInstance.viewForModule(
"AddRatingApp",
initialProperties: ["identifier": mixer.identifier, "currentRating": currentRating])
native code の変更を行ったので、アプリをリビルドしてrunしてください。
addRatingボタンをクリックしてください。
2016-09-21 17:49:23.075 [info][tid:com.facebook.react.JavaScript] Running application "AddRatingApp" with appParams: {"rootTag":1,"initialProps":{"currentRating":0,"identifier":1}}. __DEV__ === true, development-level warning are ON, performance optimizations are OFF
native appとコミュニケーションする。
nativeとreactをブリッジしたというには、今の段階ではいささか不十分です。
cancelボタンを押すと、native画面に戻るようにしてみましょう。
Mixerフォルダの中にAddRatingManager.swiftを作成してください。
import Foundation
import React
@objc(AddRatingManager)
class AddRatingManager: NSObject {
var bridge: RCTBridge!
@objc func dismissPresentedViewController(_ reactTag: NSNumber) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: reactTag) {
let presentedViewController: UIViewController! = view.reactViewController()
presentedViewController.dismiss(animated: true, completion: nil)
}
}
}
}
このクラスは
dismissPresentedViewController(_:)
を含んでいます。このメソッドは、root viewのタグを含んでいます。このメソッドは、コードをmain threadで実行します。
次に空のobjective-cファイルを作成します。AddRativgManagerBridge.mをMixerフォルダに作成してください。
その際、Objective-C briding headerファイルを作るかというポップアップが出るので、作らないを選択してください。
作成したファイルに、以下を加えてください。
#import "RCTBridgeModule.h"
@interface RCT_EXTERN_MODULE(AddRatingManager, NSObject)
RCT_EXTERN_METHOD(dismissPresentedViewController:(nonnull NSNumber *)reactTag)
@end
ブリッジがイニシャライズされた時、カスタムされたnative moduleは、RCT_EXTERN_MODULEとregisterによって宣言されます。AddRatingManagerはNativeModulesのリストに加わります。このブリッジはRCT_EXTARN_METHODの宣言を通して、native側に渡されます。
次にAddRatingApp.jsをcancelボタンをタップした時にnativeのコードを呼ぶように変えましょう。
最初に、 NativeModulesライブラリをインストールします。
// AddRatingApp.js
...
NativeModules,
} from 'react-native';
それでは、AddRatingManagerをnative moduleから加えましょう。
// AddRatingApp.js
const { AddRatingManager } = NativeModules;
_renderNavLeftItemを修正してください。onPressを以下のように変えましょう。
onPress={() => {
AddRatingManager.dismissPresentedViewController(this.props.rootTag);
}}
rootTag propお渡したので、react nativeのviewから戻ることができるようになりました。
rebuildしてみましょう。
cancelボタンをタップしてください。
react nativeで作った画面が消えるはずです。
Saveロジックを加える
jsフォルダの中にRating.jsという新しいファイルを作成します。
'use strict';
import React from 'react';
import ReactNative, {
StyleSheet,
Text,
View,
Image,
TouchableOpacity,
} from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 100,
alignItems: 'center',
backgroundColor: 'white',
},
instructions: {
fontSize: 20,
color: 'black',
marginBottom: 20,
},
ratings: {
flexDirection: 'row',
},
icon: {
width: 52,
height: 58,
margin: 5
},
});
class Rating extends React.Component {
// 1
_onPress(rating) {
console.log("Rating selected: " + rating);
}
render() {
// 2
var ratings = [];
for (var k = 1; k <= 5; k++) {
var key = 'rating-'+k;
// 3
var ratingImage = (k <= this.props.rating) ?
<Image style={styles.icon} source={require('./images/star_on.png')} /> :
<Image style={styles.icon} source={require('./images/star_off.png')} />;
// 4
var rating =
<TouchableOpacity key={key} onPress={this._onPress.bind(this, k)}>
{ratingImage}
</TouchableOpacity>;
ratings.push(rating);
}
// 5
return (
<View style={styles.container}>
<Text style={styles.instructions}>What did you think about this mixer?</Text>
<View style={styles.ratings}>
{ratings}
</View>
</View>
);
}
}
module.exports = Rating;
ごく普通のreact componentですね。
AddRatingApp.jsを開いてください。
まず最初に
const Rating = require('./Rating');
次に_renderScerne()を以下のコードに書き換えてください。
_renderScene(route, navigator) {
return (
<Rating
title={route.title}
navigator={navigator}
rating={this.props.currentRating}
/>
);
}
add rating viewに移動して、シュミレーターをリロードしてください。以下の画面が表示されましたか?
星を押して、xcodeに以下のようなlogが出たらブリッジ成功です。
2016-09-21 18:16:03.391 [info][tid:com.facebook.react.JavaScript] Rating selected: 2
saveロジックが完成したら星に色がつきます。
まず、native側から取り掛かりましょう。
addRatingManager.swiftを開き、以下のコードをclassの一番後ろに貼り付けます。
@objc func save(_ reactTag: NSNumber, rating: Int, forIdentifier identifier: Int) -> Void {
// Save rating
UserDefaults.standard.set(rating, forKey: "currentRating-\(identifier)")
dismissPresentedViewController(reactTag)
}
これはritingに渡されたNSUserDefaultsを保存します。
次にAddRatingManagerBridge.mを開き、以下のコードをendの前に加えます。
RCT_EXTERN_METHOD(save:(nonnull NSNumber *)reactTag rating:(NSInteger *)rating forIdentifier:(NSInteger *)forIdentifier)
これで、nativet側の変更は終了です。xcodeからrebuildとrunしてください。
今の所見た目には変化はありません。
次にjsの実装に移ります。
AddRatingApp.jsを開き、constructorをinedtifierとrantingを保存するために作ります。
constructor(props) {
super(props);
this.state = {
identifier: props.identifier,
currentRating: props.currentRating,
}
}
次に以下のハンドラをユーザーがratingを選択できるように書いてください。
onRatingSelected(selectedRating) {
this.setState({
currentRating: selectedRating,
});
}
この新しいメソッドはRatingコンポーネントから呼ぶことが出来ます。
次に_renderSceneの中のRatingを以下のようにアップデートしてください。
<Rating
title={route.title}
navigator={navigator}
rating={this.state.currentRating}
ratingSelectionHandler={this.onRatingSelected.bind(this)}
/>
currectRatingをpropsから取ってくるのではなく、stateから取ってくるようにしました。
Rating.jsを開き、_onPressを修正します。
_onPress(rating) {
if (this.props.ratingSelectionHandler) {
this.props.ratingSelectionHandler(rating);
}
}
本家のチュートリアルはまだまだ続きます。
ヘッダーを表示
react-nativeの画面からswiftの画面に戻れるようにする
など。
ただ私はそこまで日本語化するのが面倒になったので、読みたい方は
https://www.raywenderlich.com/945-react-native-tutorial-integrating-in-an-existing-app
を読んでみてください。
ここまで来て丸投げした。
でも、ワイ思うんや!
丸投げこそ仕事のコツやって!