「起業したい!良いアイデアを思いついた!でも周りにエンジニアいない…よし自分でアプリ作ってみよう」
— 川西発之 / 陳発暉 (@ho_ttu) May 10, 2018
そんな人に向けた、添付動画のアプリを実際に作りながら読み進めていくチュートリアルnoteです。
React Nativeで爆速モックアップ/プロトタイプアプリを作ろう 1/3|notehttps://t.co/Op93sfbC6g pic.twitter.com/bxGZJF6yK6
↑このシリーズの最終完成形
↑前回記事までの目標
↑この記事での目標
この記事で得れる物
・上図のスマホアプリが作れるようになる
・React Nativeの流れを図で直感的にわかる
・なぜこのようにプログラミングをしたのかまでわかる
この記事の対象者
・アプリ開発経験そんなない方
・Web系開発経験そんなない方
・でもまあほんのちょっとプログラミングかじった事ある方(かじるどころかなめた程度の方でもOKです!)
大丈夫です、僕も当時こんな感じでした。Swift(iOS用プログラミング言語)もKotlin(Android用プログラミング言語)も知らなければ、よく聞くjQueryすら触った事ないほどJavaScript(Web系プログラミング言語)に対する知識もカスでした泣。それでも習得できたので大丈夫です。筆者が水先案内人として読者の方々を、「後は自分でググればいける」というレベルまで引き上げます。また、Windowsでも開発できますが、本記事ではMacで作る事を想定してます。
React Nativeとは?
本来ならiOSアプリとAndroidアプリは別言語で書かなければいけなかったのですが、2015年くらいにiOSとAndroidの両アプリを同時に書けちゃう画期的なプログラミング言語をFacebook社が開発しました。それがReact Nativeです(正確にはReact Nativeはプログラミング言語ではなくて、実際はJavaScriptって言語で書きます)。
処理速度も速く、高く評価されており、FacebookやInstagramは元よりAirbnbなどの超有名アプリもReact Nativeにどんどん乗り換えています。日本製アプリで言うとメルカリやProgateもReact Nativeだそうです。理由は一言で言うと、開発スピードが速いからです。トレンドの移り変わりが激しい現代のITベンチャーにおいてこれほど嬉しい利点は他にないでしょう。
この記事の目的は?
……と、つまり激アツなプログラミング言語なのですがいかんせん日本語のまとまった情報が少なく、僕が学ぶ際に非常に苦労したのでこのチュートリアル記事を執筆することにしました。
「起業したい!良いアイデアを思いついた!でもアプリ作ったことないし、だからといって周りにエンジニアもいない……よしここは一丁自分でプロトタイプを作ってみよう。Instagram立ち上げた人もメルカリの創業者も最初はプログラミング未経験から始めたんだし!」
……と意気込んで調べてみるものの、出てくるQiitaなどの記事はどれも既にある程度の知識を持ってる前提で話が進んでることがしばしば。いやソースコードだけ貼られてもわかんねえし、みたいな。
そこでこのReact Nativeを初学者でも簡単に学べれるようになれば、iOS/Androidの両アプリを一気に作れちゃうし、超速攻で手元にある実機で動作確認できるし、リリース後はApp Storeの審査を待たずに細かな修正をできるし……と言ったベンチャーにとって非常に有利になる武器を持った状態からスタートできます。そして将来性ありまくりな言語であることは間違いなく、何より僕自身「アプリってこんな簡単に作れるんだ!」と感動したのでその感動をもっと広めたく執筆しました。
SwiftもKotlinもJavaScriptも知らなかった筆者がReact Native(≒JavaScript)を学ぶ際に実際に引っかかった点を主に、「もっとこうやって教えてくれたらいいのに」という視点から超初心者(かつての自分)向けにスクショ豊富で書いてます。わかりやすさを優先しているためざっくりとした説明が多く、本当の意味での正確性には欠けているかもしれません。ただ読み進めてコード書き写してくだけでそれなりのアプリが作れるようになる、そんな記事にしたいと思います。実際にあなたのスマホ上で作ったアプリを動かしますので、スマホからではなくPCからの閲覧推奨です。
作るアプリは"旅行記アプリ"です。過去に自分が行った国と日付を入力し、その時の思い出の写真とその旅行の3段階評価を付けて保存していく、というアプリです(Trip + record = Treco …のつもり)。まあ使い道があるかどうかはいいとして(笑)、このアプリにはよくある要素の
- 真ん中が
+
プラスボタンになってるタブ構成 & 画面遷移 - 日付等を選ぶプルダウンメニュー
- 地図や画像の配置
- レビューの中から良い / 普通 / 悪いを仕分けする方法
などが多く積み込まれており、かつ完成後に自分で色々操作してニヤニヤできる(コレ重要)という事で選びました。あとはこれらの基礎要素を応用して組み替えたりしていけば、大体頭の中で想像しているアプリを実装できるようになりますし、何よりこのチュートリアルシリーズを読み終わってる頃には、自力で検索して必要な情報だけを探せれるようになっています。
前回記事では、初回起動時に表示されるウェルカム画面(左右にスライドできて各ページにアプリの説明が書いてあるやつ)を作りましたね。本記事では以上のような旅行記アプリの、旅の記録一覧がズラーっと並ぶホーム画面を実装します。2回目以降にアプリを立ち上げた時に最初に表示される画面ですね。この回からグッと学ぶ事増えますが大丈夫です、スクショ&図解付きでわかりやすく説明していきます。
1. ウェルカム画面編
2. ホーム画面編 ← 今ココ
3. プラスボタン編
画面遷移を付けよう
まずはモバイルアプリの基本である、画面下部のタブによる画面遷移から始めましょう。イメージはこんな感じです。
前回作ったWelcomeScreen
から → メインである
HomeScreen
/ AddScreen
/ ProfileScreen
の3画面に飛んだ後は、再度WelcomeScreen
に戻らせないためにあえてタブを隠して行き来不可にします。一方で、(当然ですが)メインの3画面間はタブを出して行き来可能にさせます。本記事ではこのHomeScreen
を完成させます。
幸運な事にReact Nativeには画面遷移の実装を超楽にしてくれるreact-navigation
と言うものがあるので、前回記事と同じようにターミナルで
$ npm
コマンドを使って外部からインストールしましょう。
(2018/5/7にreact-navigation
がver. 2へアプデで大幅仕様変更されましたが、本noteでは早速ver. 2へ対応しております)
$ npm install react-navigation
んで$ npm
した時のお約束で、念のため再度$ npm install
します。
$ npm install
これで画面遷移を作る準備ができました。早速App.js
に行き先ほどインストールしたreact-navigation
からcreateBottomTabNavigator
をインポートします。ついでにHomeScreen
/ AddScreen
/ ProfileScreen
のメイン3画面もインポートしちゃいましょう(中身は後で作ります)。
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { createBottomTabNavigator } from 'react-navigation';
import WelcomeScreen from './screens/WelcomeScreen';
import HomeScreen from './screens/HomeScreen';
import AddScreen from './screens/AddScreen';
import ProfileScreen from './screens/ProfileScreen';
export default class App extends React.Component {
render() {
// ゴニョゴニョ…
}
}
先ほどインポートしたcreateBottomTabNavigator()
関数はタブ遷移を作ってくれる優れもので、使い方はこうです↓。
import 画面1 from '画面1/の/保存場所';
import 画面2 from '画面2/の/保存場所';
import 画面3 from '画面3/の/保存場所';
const 変数名 = createBottomTabNavigator({
ID1: { screen: 画面1 },
ID2: { screen: 画面2 },
ID3: { screen: 画面3 }
});
createBottomTabNavigator()
の中に波括弧{ }
を使ってJavaScriptオブジェクトを書くと、スマホ画面下にタブが簡単に作れちゃいます。
では次に、App
コンポーネントの中のrender()
関数内にNavigatorTab
という名の変数を作ります(const
という魔法の接頭辞を忘れずに…)。そのNavigatorTab
の中に、createBottomTabNavigator()
関数を使ってタブ遷移の詳細を追記して行きます。
export default class App extends React.Component {
render() {
const NavigatorTab = createBottomTabNavigator({
welcome: { screen: WelcomeScreen },
main: createBottomTabNavigator({
homeStack: { screen: HomeScreen },
addStack: { screen: AddScreen },
profileStack: { screen: ProfileScreen }
})
});
return (
<View style={styles.container}>
<NavigatorTab />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
// ↓この文消さないと`react-navigation`が上手く動かず、画面真っ白になっちゃう
//alignItems: 'center',
justifyContent: 'center',
},
});
注意点が2つあります。1つ目は冒頭のイメージ図でも示した通り、BottomTabNavigator
の中に更にもう1つBottomTabNavigator
が入れ子になっている事です(プログラミングではこういう事よくあります)。
App.js
2つ目はソースコード最下部のstylesオブジェクトでalignItems: 'center'
を消すかコメントアウトすることです。alignItems: 'center'
があると何故かreact-navigation
が上手く動かず、画面真っ白になってしまいます。
※ JavaScriptのコメントアウトとは先頭にスラッシュ2つ//
を付けることで、コンピューター側からするとその文はなかったことになります。でも消すにはもったいないとか、メモ書きを残すとか、そういった人間側の都合でコメントアウトは頻繁に使われます。
次に先ほどまだ作ってもないのにインポートした、HomeScreen
/ AddScreen
/ ProfileScreen
のメイン3画面を新規作成しましょう。WelcomeScreen.js
と同じscreens
フォルダの下に新たにHomeScreen.js
, AddScreen.js
, ProfileScreen.js
を作成し、
以下の雛形をコピペしていきましょう↓。
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
class HomeScreen extends React.Component {
render() {
return (
<View style={{ flex: 1, justifyContent: 'center' }}>
<Text>This is HomeScreen</Text>
</View>
);
}
}
export default HomeScreen;
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
class AddScreen extends React.Component {
render() {
return (
<View style={{ flex: 1, justifyContent: 'center' }}>
<Text>This is AddScreen</Text>
</View>
);
}
}
export default AddScreen;
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
class ProfileScreen extends React.Component {
render() {
return (
<View style={{ flex: 1, justifyContent: 'center' }}>
<Text>This is ProfileScreen</Text>
</View>
);
}
}
export default ProfileScreen;
これでタブ遷移の準備は整ったので、動作確認してみましょう。 前回と同じくExpo XDEのDevice
→ Open on iOS simulator
をクリックします。
おっと、1つ目の(=親の)BottomTabNavigator
つまりwelcome
〜main
のタブをまだ非表示にしてなかったので、タブの上に更にタブが乗っかっちゃってますね汗。ここで親BottomTabNavigator
のタブを非表示にする前に、タブ遷移文の構文をスッキリさせるために新たにMainTab
という変数を作り、子BottomTabNavigator
の中身をその中にぶち込みます。
const MainTab = createBottomTabNavigator({
homeStack: { screen: HomeScreen },
addStack: { screen: AddScreen },
profileStack: { screen: ProfileScreen }
});
const NavigatorTab = createBottomTabNavigator({
welcome: { screen: WelcomeScreen },
main: { screen: MainTab }
});
この時点では書き方をスッキリさせたたけですので、アプリの挙動自体は何ら変わっていません。次に(やっと)親BottomTabNavigator
のタブを非表示にします。そのためには、
createBottomTabNavigator({ 画面達 }, { タブに関する設定など });
↑こんな感じでcreateBottomTabNavigator()
関数の2つ目の波括弧{ }
内にタブの設定を書き込んでいきます。タブを非表示にする設定は、navigationOptions: { tabBarVisible: false }
です↓。
const MainTab = createBottomTabNavigator({
homeStack: { screen: HomeScreen },
addStack: { screen: AddScreen },
profileStack: { screen: ProfileScreen }
});
const NavigatorTab = createBottomTabNavigator({
welcome: { screen: WelcomeScreen },
main: { screen: MainTab }
}, {
navigationOptions: { tabBarVisible: false } // ←追記部分
});
こうすることでwelcome
〜main
のタブを非表示にできます。また、そうするとWelcomeScreen
からメインタブに飛ぶこともできなくなっちゃうので、screens/WelcomeScreen.js
のボタンをいじります。より具体的には前回作ったonStartButtonPress()
関数を「アラートを出す」から→「メインタブに飛ばす」に変えます。
class WelcomeScreen extends React.Component {
onStartButtonPress = () => {
Alert.alert(
'Alert',
'The button was pressed',
[
{ text: 'OK' },
],
{ cancelable: false }
);
}
// ゴニョゴニョ…
}
変更前「アラートを出す」
↓
class WelcomeScreen extends React.Component {
onStartButtonPress = () => {
this.props.navigation.navigate('main');
}
// ゴニョゴニョ…
}
変更後「メインタブに飛ばす」
指定の画面に遷移する魔法の文は、this.props.navigation.navigate('指定ID')
です。this
は前回説明した通り「同じ屋根の下」(= ここではWelcomeScreenコンポーネント)という意味を表していますが、問題はprops
とnavigation
です。特にprops
は厄介者で、後に登場するもう1つの曲者state
との比較で記事1本書けちゃうほどなので、ここでは深く立ち入りません。とにかく、指定の画面に遷移する時は例外を除いてほとんどthis.props.navigation.navigate('指定ID')
で済みます。
ではこの状態で動作確認してみましょう!
この調子で画面遷移を全て完成させちゃいましょう!ここでMainTab
の各タブにStackNavigator
を追加します。StackNavigator
とはよくあるあの、画面が奥にどんどん突き進んで行って、左上の”戻る”ボタン or 画面上を左から右へヌルッとスライドする事によって1個前の画面に戻れるやつです。アプリの設定画面などでよくあるやつです。
またStackNavigator
中の特定の画面では、途中で他のタブへ移動させないようにするために、タブを隠すよう設定します。ではcreateStackNavigator()
関数を使って実際に作っていきましょう。まずはcreateBottomTabNavigator()
関数の時と同じく、createStackNavigator()
関数をreact-navigation
からインポートし、新たに出てきたDetailScreen
/ Setting1Screen
/ Setting2Screen
もインポートします(中身は後で書きます)。
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { createBottomTabNavigator, createStackNavigator } from 'react-navigation';
import WelcomeScreen from './screens/WelcomeScreen';
import HomeScreen from './screens/HomeScreen';
import DetailScreen from './screens/DetailScreen';
import AddScreen from './screens/AddScreen';
import ProfileScreen from './screens/ProfileScreen';
import Setting1Screen from './screens/Setting1Screen';
import Setting2Screen from './screens/Setting2Screen';
export default class App extends React.Component {
render() {
// ゴニョゴニョ…
}
}
また、新たにHomeStack
, AddStack
, ProfileStack
という名の変数を作り、その変数達にStackNavigator
の詳細を書き込んでいきます。また、MainTab
の内のscreen:
達も合わせてHomeStack
, AddStack
, ProfileStack
に変更してます↓。
const HomeStack = createStackNavigator({ // ←追記部分
home: { screen: HomeScreen },
detail: { screen: DetailScreen }
});
const AddStack = createStackNavigator({ // ←追記部分
add: { screen: AddScreen }
});
const ProfileStack = createStackNavigator({ // ←追記部分
profile: { screen: ProfileScreen },
setting1: { screen: Setting1Screen },
setting2: { screen: Setting2Screen }
});
const MainTab = createBottomTabNavigator({
homeStack: { screen: HomeStack }, // ←変更部分
addStack: { screen: AddStack }, // ←変更部分
profileStack: { screen: ProfileStack } // ←変更部分
});
StackNavigator
の下準備が整ったところで、動作確認のために
-
HomeScreen
⇆DetailScreen
を行き来するボタン -
AddScreen
→HomeScreen
に戻るボタン(一方通行) -
ProfileScreen
⇆Setting1Screen
⇆Setting2Screen
を行き来するボタン
を付けましょう。screens/HomeScreen.js
には前回インストールしたreact-native-navigation
のButton
を付けます。んでonPressプロパティに「ボタンを押されたら'detail'に飛ぶ」ようにアロー関数( ) => { }
を使って指示します。ただしアロー関数で指示したい内容がたった1文(今回はthis.props.navigation.navigate('detail')
のみ)だけの場合は、( ) => { }
の後ろの波括弧{ }
は略しても良いというルールがあります↓。
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Button } from 'react-native-elements'; // ←追記部分
class HomeScreen extends React.Component {
render() {
return (
<View style={{ flex: 1, justifyContent: 'center' }}>
<Text>This is HomeScreen</Text>
<Button // ←追記部分
title="Go to DetailScreen"
onPress={() => this.props.navigation.navigate('detail')}
/>
</View>
);
}
}
export default HomeScreen;
screens/DetailScreen.js
は新規作成します。特にボタンとかはまだ付けないです↓。
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
class DetailScreen extends React.Component {
render() {
return (
<View style={{ flex: 1, justifyContent: 'center' }}>
<Text>This is DetailScreen</Text>
</View>
);
}
}
export default DetailScreen;
screens/AddScreen.js
にはreact-native-navigation
のIcon
を付けます。バツ印を表現するために、nameプロパティには"close"を選びました。こちらのonPressプロパティには「ボタンを押されたら'home'に飛ぶ」ようにアロー関数( ) => { }
を使って指示します↓。
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Icon } from 'react-native-elements'; // ←追記部分
class AddScreen extends React.Component {
render() {
return (
<View style={{ flex: 1, justifyContent: 'center' }}>
<Text>This is AddScreen</Text>
<Icon // ←追記部分
name="close"
onPress={() => this.props.navigation.navigate('home')}
/>
</View>
);
}
}
export default AddScreen;
screens/ProfileScreen.js
にはscreens/HomeScreen.js
と同じようにreact-native-navigation
のButton
を付けます。んでonPressプロパティに「ボタンを押されたら'setting1'に飛ぶ」ようにアロー関数( ) => { }
を使って指示します↓。
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Button } from 'react-native-elements'; // ←追記部分
class ProfileScreen extends React.Component {
render() {
return (
<View style={{ flex: 1, justifyContent: 'center' }}>
<Text>This is ProfileScreen</Text>
<Button // ←追記部分
title="Go to Setting1Screen"
onPress={() => this.props.navigation.navigate('setting1')}
/>
</View>
);
}
}
export default ProfileScreen;
screens/DetailScreen.js
は新規作成します。react-native-elements
のボタンも付けちゃいます。onPressプロパティには「ボタンを押されたら'setting2'に飛ぶ」ようにアロー関数( ) => { }
を使って指示します↓。
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Button } from 'react-native-elements';
class Setting1Screen extends React.Component {
render() {
return (
<View style={{ flex: 1, justifyContent: 'center' }}>
<Text>This is Setting1Screen</Text>
<Button
title="Go to Setting2Screen"
onPress={() => this.props.navigation.navigate('setting2')}
/>
</View>
);
}
}
export default Setting1Screen;
screens/Setting2Screen.js
は新規作成します。特にボタンとかはまだ付けないです↓。
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
class Setting2Screen extends React.Component {
render() {
return (
<View style={{ flex: 1, justifyContent: 'center' }}>
<Text>This is Setting2Screen</Text>
</View>
);
}
}
export default Setting2Screen;
screens
フォルダの中にあるWelcomeScreen.js
以外の6ファイルは全部いじりました。整理すると、
・編集:HomeScreen.js
, AddScreen.js
, ProfileScreen.js
・新規作成:DetailScreen.js
, Setting1Screen.js
, Setting2Screen.js
です。ではスペルミスがないかチェックして動作確認してみましょう!
2階層目以降の画面からは、左上の”戻る”ボタン(今は見えないですけど) or 画面上を左から右へヌルッとスライドする事によって1個前の画面に戻れる事が確認できますね。本当は左上に "<" こんな感じのバック矢印ボタンが表示されるはずなんですけど、iOSシミュレーターが遅すぎて表示されません泣(実機テストならすぐ描画されると思います!)。またAddScreen
は特別で、AddStack
の中にAddScreen
の1画面しかないため、戻るもくそもありません。
次に任意の画面でタブを隠す設定を書き込みましょう。イメージ図をもう一度確認すると、
・HomeStack
はDetailScreen
(=2階層目)以降
・AddStack
はAddScreen
(=1階層目)以降
・ProfileStack
はSetting1Screen
(=2階層目)以降
からタブを隠してますね。そのためにはApp.js
に戻って、各StackのnavigationOptions
で設定をします。「〜の中の」を意味するドット.
を使ってnavigationOptions
に行き、そこにオブジェクトを代入します↓。
各Stack.navigationOptions = { JavaScripオブジェクト };
しかし今回は、「今自分が何階層目に居るのかを知ってそれに応じてタブを隠す/隠さないを選択する」というちょっとしたロジックが必要なので、そう単純には行きません。ここでもまたアロー関数( ) => { }
が活躍します↓。
各Stack.navigationOptions = (入力) => {
// ゴニョゴニョ…
return { JavaScripオブジェクト } // ←出力
};
「今自分が何階層目に居るのか」を知るために、今回は{ navigation }
を入力としてぶち込みます(波括弧{ }
を忘れないでください!)。また今回出力として返すオブジェクトはtabBarVisible:
です。
・tabBarVisible: true
→ タブを表示(デフォルト)
・tabBarVisible: false
→ タブを非表示
例えばProfileStack
を例にすると、
2階層目以降からタブを隠す(tabBarVisible:
をfalse
にする)という事は、逆に言えば1階層目だけはタブを表示(tabBarVisible:
をtrue
にする)しなければいけません。今何階層目に居るかは、先ほど入力として入れたnavigation
を使ってnavigation.state.index
と書くとわかります。という事は、
「navigation.state.index
の値が1階層目を示している時にだけtrue
になり、それ以外はfalse
になる」
ような魔法の文を書ければぴったし解決できます。どうやって書くんだそんなの……と思われるかもしれませんが、それが都合の良い事にどのプログラミング言語にもあるんです。それは、左辺と右辺が等しいかどうか見比べる===
というフレーズです。左辺 === 右辺
は、
・もし左辺と右辺が一緒だったらtrue
を吐き出す
・もし左辺と右辺が一緒じゃなかったらfalse
を吐き出す
という今回のケースにぴったしなやつです(まあ今後も===
はちょくちょく出てきます)。更にここで注意なのは、プログラミングの世界では数字は1からではなく0から始まる事です。前回配列を扱うときもそうでしたね。つまり「navigation.state.index
の値が1階層目を示している時にだけtrue
になり、それ以外はfalse
になる」は一見、
navigation.state.index === 1 // ←2階層目を表す
と思いがちですがこれは2階層目を表すので、本当は
navigation.state.index === 0 // ←1階層目を表す
です。長かったですが結局答えはこちらです↓。
ProfileStack.navigationOptions = ({ navigation }) => {
return {
tabBarVisible: (navigation.state.index === 0)
};
};
こうすることで、2階層目(navigation.state.index
の値が1)以降はタブが非表示になります。
同じことをHomeStack
とAddStack
にも行いましょう。ただし注意はAddStack
は1階層目(navigation.state.index
の値が0)からタブを隠すので、その一個前の数字ということで-1を指定しています。-1(=0階層目)なんて存在しないけど。
export default class App extends React.Component {
render() {
// `HomeStack`について
const HomeStack = createStackNavigator({
home: { screen: HomeScreen },
detail: { screen: DetailScreen }
});
// 1階層目以外はタブを隠す
HomeStack.navigationOptions = ({ navigation }) => {
return {
tabBarVisible: (navigation.state.index === 0)
};
};
// `AddStack`について
const AddStack = createStackNavigator({
add: { screen: AddScreen }
});
// 0階層目以外(つまり全階層)はタブを隠す
AddStack.navigationOptions = ({ navigation }) => {
return {
tabBarVisible: (navigation.state.index === -1) // ←0じゃなくて-1
};
};
// `ProfileStack`について
const ProfileStack = createStackNavigator({
profile: { screen: ProfileScreen },
setting1: { screen: Setting1Screen },
setting2: { screen: Setting2Screen }
});
// 1階層目以外はタブを隠す
ProfileStack.navigationOptions = ({ navigation }) => {
return {
tabBarVisible: (navigation.state.index === 0)
};
};
// `HomeStack`, `AddStack`, `ProfileStack`を繋げて`MainTab`に
const MainTab = createBottomTabNavigator({
homeStack: { screen: HomeStack },
addStack: { screen: AddStack },
profileStack: { screen: ProfileStack }
});
// `WelcomeScreen`と`MainTab`を繋げて`NavigatorTab`に
const NavigatorTab = createBottomTabNavigator({
welcome: { screen: WelcomeScreen },
main: { screen: MainTab }
}, {
navigationOptions: { tabBarVisible: false }
});
// `NavigatorTab`を描画
return (
<View style={styles.container}>
<NavigatorTab />
</View>
);
}
}
これで動作確認してみましょう!
ちゃんと希望の階層以降ではタブが隠れてますね!こうする事によって例えば、AddScreen
での入力作業を終える前に別のタブへ移動しちゃう事を防ぐ、とかができます。
これで画面遷移自体の大枠はできたので、次はこの味気ないヘッダーやタブ達を装飾していきます。ややこしいんですけど、ここでもまたnavigationOptions
が出てくるんですよね汗。
まず始めにPlatform
というのをreact-native
からインポートします。これは、アプリを開いてるのがiOSなのかAndroidなのか教えてくれるヤツです。
import { StyleSheet, Text, View, Platform } from 'react-native';
次にヘッダーに共通する点をひとまとめのJavaScript オブジェクトにしてheaderNavigationOprions
という名の変数に格納します。
const headerNavigationOptions = {
headerStyle: {
backgroundColor: 'deepskyblue',
marginTop: (Platform.OS === 'android' ? 24 : 0)
},
headerTitleStyle: { color: 'white' },
headerTintColor: 'white',
};
・headerStyle:
…ヘッダー全般。更ににオブジェクトが入る
・backgroundColor:
…ヘッダーの背景色
・marginTop:
…ヘッダー自身より上の領域に空白を入れる
・headerTitleStyle:
…ヘッダータイトル全般。更にオブジェクトが入る
・color:
…ヘッダータイトルの文字色
・headerTintColor:
…画面左上の戻るボタンの文字色
です。marginTop
だけちょっとトリッキーな構文になってますね。これは三項演算子と言って(名前なんてどうでも良いですが)、「もし〜〜だったら〇〇する、もしそうじゃなかったら××する」という意味をハテナ記号?
とコロン:
で表すものです。
~~ ? 〇〇 : ×× // ←もし〜〜だったら〇〇する、そうじゃなかったら××する
この三項演算子~~ ? 〇〇 : ××
とちょっと前に出てきた===
はむちゃくちゃ相性が良いものでして、こんな風なことが書けちゃいます↓。
左辺 === 右辺 ? 〇〇 : ×× // ←もし左辺と右辺が一緒だったら〇〇、そうじゃなかったら××
よって先ほどのmarginTop
の構文は、
marginTop: (Platform.OS === 'android' ? 24 : 0)
もし本アプリを使用しているスマホがAndroidだったら、
marginTop: 24
そうじゃなかったら、
marginTop: 0
という意味です。Androidの時だけヘッダーの上に更にもうちょい余白が必要なんですね。
※ 上記marginTop
の件はReact Native自体のアプデ等の仕様変更によりいつか必要なくなるかもしれません。 筆者はiOSでしか動作確認を行えないため、Androidの方もし居ましたらお手数ですが情報提供お願い致します (><)
それでは、作成したheaderNavigationOprions
を各スクリーンに適応していきましょう。各スクリーンごとの装飾設定navigationOprions: { }
は、screen:
の次に書きます↓。
createStackNavigator({
画面ID1: { screen: 画面名1, navigationOptions: { /*色々*/ } },
画面ID2: { screen: 画面名2, navigationOptions: { /*色々*/ } },
.
.
.
});
まずはHomeStack
の中のHomeScreen
に適応してみましょう。
const headerNavigationOptions = {
headerStyle: {
backgroundColor: 'deepskyblue',
marginTop: (Platform.OS === 'android' ? 24 : 0)
},
headerTitleStyle: { color: 'white' },
headerTintColor: 'white',
};
const HomeStack = createStackNavigator({
home: {
screen: HomeScreen,
navigationOptions: {
...headerNavigationOptions,
headerTitle: 'Treco', // ←アプリ名は何でも良い
headerBackTitle: 'Home'
},
},
detail: { screen: DetailScreen }
});
。。。はい、違和感ある変な書き方ですね笑。でもドット3連続...
をheaderNavigationOprions
オブジェクトの前に付ける、で合ってます。
これはJavaScript特有の書き方で、ドット3連続...
は「JavaScriptオブジェクト(or 配列)の中身をこの場に展開する」って意味です。つまりこの場合、headerNavigationOprions
の中身である
・headerStyle:
…ヘッダー全般。更ににオブジェクトが入る
・backgroundColor:
…ヘッダーの背景色
・marginTop:
…ヘッダー自身より上の領域に空白を入れる
・headerTitleStyle:
…ヘッダータイトル全般。更にオブジェクトが入る
・color:
…ヘッダータイトルの文字色
・headerTintColor:
…画面左上の戻るボタンの文字色
これらが一気にnavigationOprions: { }
内に展開されます。こんな感じに↓。
const HomeStack = createStackNavigator({
home: {
screen: HomeScreen,
navigationOptions: {
// ここから`headerNavigationOprions`の中身の展開開始〜
headerStyle: {
backgroundColor: 'deepskyblue',
marginTop: (Platform.OS === 'android' ? 24 : 0)
},
headerTitleStyle: { color: 'white' },
headerTintColor: 'white',
// 〜ここまで`headerNavigationOprions`の中身の展開終了
headerTitle: 'Treco',
headerBackTitle: 'Home'
},
},
detail: { screen: DetailScreen }
});
後は足りない項目(他のスクリーン達と共通ではない部分)を付け足します。
・headerTitle:
…ヘッダーのタイトル
・headerBackTitle:
…一個奥の画面から戻る際の、左上の戻るボタンの文字
です。動作確認してみると…
おお、綺麗な水色のヘッダーが表示されました!ヘッダーができると一気にアプリ感が増しますね。次はHomeStack
の中のDetailScreen
に適応してみましょう。
const HomeStack = createStackNavigator({
home: {
screen: HomeScreen,
navigationOptions: {
...headerNavigationOptions,
headerTitle: 'Treco',
headerBackTitle: 'Home'
},
},
detail: {
screen: DetailScreen,
navigationOptions: {
...headerNavigationOptions,
headerTitle: 'Detail',
}
}
});
HomeScreen
と違ってDetailScreen
はもうこれ以上奥に画面遷移しない予定なので、headerBackTitle:
は要りません。こんな感じになるはずです↓。
iOSシミュレーターは実機に比べて処理速度遅いので、もしかしたら画面左上の戻るボタンの矢印マーク" < "は出てくるのが遅いかもしれません。数秒待つと描画されます(矢印マークが出てなくてもボタン自体は押せます)。
ちょっとAddStack
は飛ばして、次はProfileStack
のスクリーン達にnavigationOprions: { }
を同じ要領で追加していきましょう↓。
const ProfileStack = createStackNavigator({
profile: {
screen: ProfileScreen,
navigationOptions: {
...headerNavigationOptions,
headerTitle: 'Treco',
headerBackTitle: 'Profile'
}
},
setting1: {
screen: Setting1Screen,
navigationOptions: {
...headerNavigationOptions,
headerTitle: 'Setting 1',
// headerBackTitle: 'Setting 1' は要らない。
}
},
setting2: {
screen: Setting2Screen,
navigationOptions: {
...headerNavigationOptions,
headerTitle: 'Setting 2',
}
}
});
ここで注意は、Setting1Screen
にはもう1階層奥にSetting2Screen
があるのにも関わらず、headerBackTitle:
は要らないという点です。なぜならHomeScreen
やProfileScreen
と違って、ヘッダーに表示したいタイトルheaderTitle:
と、1階層奥から戻る時の戻るボタンに表示したいタイトルheaderBackTitle:
が一緒の文字だからです。navigationOprions: { }
は、もしheaderBackTitle:
が特別指定されていなかったらheaderTitle:
の文字を流用するという暗黙のルールがあります。逆に言えば、HomeScreen
やProfileScreen
はある意味トップ画面なのでヘッダーにはアプリ名("Treco")を表示したいけど、1階層奥の画面から戻る際は別の文字("< Home" や
"< Profile")を表示したいという時は別途headerBackTitle:
を指定しなきゃいけません。
最後に特別扱いのAddStack
のAddScreen
にnavigationOprions: { }
を追加します。AddScreen
は実際に保存したい旅行の詳細情報(国、日付、写真、評価)を入力する画面です。本来StackNavigator
はreact-navigation
側がデフォルトでヘッダーを用意してくれるのですが(HomeStack
やProfileStack
のように)、AddScreen
は上記の使い道の都合上、デフォルトヘッダーでは機能が足りないので自作ヘッダーを後々作成する予定です。なので今はデフォルトヘッダーをなしにする設定をnavigationOprions: { }
に追記しましょう↓。
const AddStack = createStackNavigator({
add: {
screen: AddScreen,
navigationOptions: {
header: null
}
}
});
簡単ですね。header:
に「無し」という意味のnull
を指定します(false
とはまた別です)。
これでヘッダー周りは完了しました。次はタブにアイコン画像を埋め込むためにMainTab
をいじるのですが、その前にちょっとAndroid用の下準備を。
const MainTab = createBottomTabNavigator({
homeStack: { screen: HomeStack },
addStack: { screen: AddStack },
profileStack: { screen: ProfileStack }
}, {
swipeEnabled: false, // Android用
});
iOSとAndroidで同じ挙動にするためにswipeEnabled:
をfalse
にセットするだけでOKです。それではタブにアイコン画像を埋め込むためにMainTab
の中の各〇〇Stack
に(またもや)navigationOprions: { }
を追加していきます。今度は、
・tabBarIcon:
…タブバーのアイコン
・title:
…タブバーに表示されるタイトル(ヘッダータイトルとは別)
の2つです。まずこちらからhome.png
, add.png
, profile.png
の3つの画像をダウンロードし、assets
フォルダの中に入れときます。
まずはMainTab
の中のhomeStack
から始めましょう↓。
const MainTab = createBottomTabNavigator({
homeStack: {
screen: HomeStack,
navigationOptions: {
tabBarIcon: ({ tintColor }) => (
<Image
style={{ height: 25, width: 25, tintColor: tintColor }}
source={require('./assets/home.png')}
/>
),
title: 'Home'
}
},
addStack: { screen: AddStack },
profileStack: { screen: ProfileStack }
}, {
swipeEnabled: false, // Android用
});
tabBarIcon:
は、アロー関数を用いてタグを適応し、title:
は純粋に 'Home' を指定しています。tabBarIcon:
についてもう少し詳しく見ていきましょう。
tabBarIcon: ({ tintColor }) => (
<Image
style={{ height: 25, width: 25, tintColor: tintColor }}
// style={{ height: 25, width: 25, tintColor }} でも可
source={require('./assets/home.png')}
/>
),
まずどこからともなくtintColor
が入力として入ってます。これはreact-navigation
側が用意してくれたもので、今どのタブにいるのかがわかる変数です。今このhomeStack
タブにいる場合は青色、その他のタブにいるときは灰色に光るという代物です。この便利なtintColor
をタグのstyleプロパティの中のtintColor:
に与えてあげれば、今いるタブだけ青く光らせることができます。タグは、
・style:
…Imageタグの装飾全般。更ににオブジェクトが入る
・height:
…画像の高さ
・width:
…画像の幅
・tintColor:
…画像の塗り潰し色
・source:
…画像の保存場所。要require()
関数
という内訳になっています。またこれはJavaScriptオブジェクトのちょっとした省略ルールですが、もし項目名と内容名が同じ場合、
{ tintColor: tintColor }
↓
{ tintColor }
と略すこともできます。お好みでどうぞ。最後にreact-native
からImage
を忘れずにインポートしましょう。
import { StyleSheet, Text, View, Image, Platform } from 'react-native';
ではこれで動作確認しましょう!
ちゃんと家の形したアイコンがタブバーに表示されて、かつ押したときは青色、押されてないときは灰色になってますね。もしかしたらiOSシミュレーターが重たいせいでアイコン画像がすぐ出てこないかもしれませんが、実機テストであればすぐ出てきます。
ではこの調子でaddStack
とprofileStack
にもnavigationOprions: { }
を付けましょう。ただここでもaddStack
は要注意で、他2つとはstyleプロパティが少し異なってます↓。
const MainTab = createBottomTabNavigator({
homeStack: {
screen: HomeStack,
navigationOptions: {
tabBarIcon: ({ tintColor }) => (
<Image
style={{ height: 25, width: 25, tintColor: tintColor }}
source={require('./assets/home.png')}
/>
),
title: 'Home'
}
},
addStack: {
screen: AddStack,
navigationOptions: {
tabBarIcon: () => (
<Image
style={{ height: 60, width: 60, tintColor: 'deepskyblue' }}
source={require('./assets/add.png')}
/>
),
title: '',
}
},
profileStack: {
screen: ProfileStack,
navigationOptions: {
tabBarIcon: ({ tintColor }) => (
<Image
style={{ height: 25, width: 25, tintColor: tintColor }}
source={require('./assets/profile.png')}
/>
),
title: 'Profile'
}
}
}, {
swipeEnabled: false, // Android用
});
ではスペルミスがないかチェック後、動作確認してみましょう!
おおお!大分それっぽくなってきましたね!addStack
を解説すると、
addStack: {
screen: AddStack,
navigationOptions: {
tabBarIcon: () => (
<Image
style={{ height: 60, width: 60, tintColor: 'deepskyblue' }}
source={require('./assets/add.png')}
/>
),
title: '',
}
},
height:
とwidth:
を両方大きめの60に設定し、tintColor:
は色を固定するために 'deepskyblue' としました。そして水色のプラスボタンの下に文字を表示させないためにtitle:
にあえて空欄の '' を指定しています。もしtitle:
を何も設定せずにいると、デフォルトで 'addStack' という文字がプラスボタンの下にうっすら描画されちゃいます。
App.js
もそろそろ終盤です。最後に、iPhone最上部の時刻や電池マークの部分(ステータスバー)を黒字から→白字に変えましょう。まずreact-native
からStatusBar
をインポートしましょう。
import { StyleSheet, Text, View, Image, StatusBar, Platform } from 'react-native';
そしてrender()
関数の最後のreturn ()
の部分で、タグの直上にタグを追記しましょう↓。
return (
<View style={styles.container}>
<StatusBar barStyle="light-content" />
<NavigatorTab />
</View>
);
barStyleプロパティを "light-content" にすればステータスバーが白字に、 "dark-content" にすればステータスバーが黒字になります。はい、これでApp.js
および画面遷移は完成です!お疲れ様でした。
スマホに情報を保存しよう
ここで次に進む前に、毎回毎回WelcomeScreen.js
が出てくるが鬱陶しいので、「初回起動時にだけ表示してそれ以降は表示しない」という実装をしましょう。ウェルカム画面の最後のページにあるスタートボタンが押されたら「ウェルカム画面表示済み」という情報をスマホに保存し、→次回起動時はまずその情報を読み込んでもし既に「ウェルカム画面表示済み」となっていたらウェルカム画面は表示せずにホーム画面へ飛ばす……という流れです。
まずは、スタートボタンが押されたら「ウェルカム画面表示済み」という情報をスマホに保存する、を実装しましょう。アプリを落とした後もスマホの機体自体に情報を残しておくためには、AsyncStorage
という所に保存します。WelcomeScreen.js
のreact-native
からAsyncStorage
をインポートします↓。
import { StyleSheet, Text, View, ScrollView, Image, Dimensions, AsyncStorage } from 'react-native';
そしてonStartButtonPress()
関数に、「AsyncStorage
に『ウェルカム画面表示済み』という情報を保存する」という文を書き加えます。'isInitialized' (初期化済みか?という意味)に 'true' をセットします↓。
onStartButtonPress = () => {
// `AsyncStorage`に『ウェルカム画面表示済み』という情報を保存する
AsyncStorage.setItem('isInitialized', 'true');
// 'main'画面へ飛ばす
this.props.navigation.navigate('main');
}
'isInitialized' も 'true' も特に指定はないので、お好みで他の文字に変えても大丈夫です。'isInitialized' に 'true' ではなく 'yes' と保存しても構いません。ただし必ず 'シングルクォーテーション' で囲ってください。なぜならAsyncStorage
は数値や変数といった値そのものを保存することはできず、文字しか保存することができません。なのでただのtrueではなく 'true' です。
しかしここでちょっと問題があります。AsyncStorage
は非同期処理と言って、他のと比べて処理に少し時間がかかるので「俺のことは待たなくていいから次の処理進めちゃってくれ!」と言わんばかりに、AsyncStorage
の処理が終わる前に次の文(ここではnavigate('main')
)に行っちゃいます。それで良い時もありますが、今回はそれだと不都合なので、「いやいやお前(AsyncStorage
)のこともちゃんと待ってやるよ」とちゃんと明記してあげる必要があります。そのためにはasync
を関数の先頭(今回はアロー関数の先頭)につけ、実際に待ってあげる文の先頭にawait
をつけます↓。
// `await`を使う関数は文頭に↓`async`と書く必要がある
onStartButtonPress = async () => {
// `AsyncStorage`に『ウェルカム画面表示済み』という情報を保存する
// `AsyncStorage`の処理を`await`(待機)してあげる
await AsyncStorage.setItem('isInitialized', 'true');
// `await`と指定された`AsyncStorage`の処理完了後に、
// 'main'画面へ飛ばす
this.props.navigation.navigate('main');
}
そしたら次は、起動時に毎回AsyncStorage
の 'isInitialized' を読み込んで、'true' だったら 'main' 画面に飛ばし、そうじゃなかったらウェルカム画面を表示するという実装をします。んでAsyncStorage
は当然書き込み時だけでなく読み込み時も非同期処理です、つまりある一定の時間がかかります。なのでWelcomeScreen.js
は 'isInitialized' が 'true' かどうか読み込んでる間、'true' なのか 'false' なのかもわからない宙ぶらりんの状態になります。その宙ぶらりんの状態を、かの有名なReact Nativeの曲者state
を使ってnull
で表し、その間は「アプリ読み込み中画面」をスマホ画面に表示させることによってユーザーに不快感を与えないようにします。
state
はよくprops
と一緒に紹介されるReact Nativeの2大特徴の内の片方で、現在のページ(コンポーネント)の状態を表すのによく使われます。例えば今回で言うと、「ウェルカム画面表示済み」がtrue
なのかfalse
なのかどっちでもないnull
なのかという3状態を管理する為の変数です。とは言え一発で理解するのは難しいですし、今後も何回も登場するのでその都度使いながら覚えていけば大丈夫です◎。
state
は形としてはJavaScriptオブジェクトであり、使い方は
this.state = {
会員番号: 001,
年齢: 23,
加入年: 2018
}
と書いて初期化し、もし途中で年齢
の値を変えたくなったら
this.setState({ 年齢: 24 }); // ← ◎
のようにthis.setState()
関数を用いて上書きします。ここで決して
this.state.年齢 = 24; // ← ×
のように、あたかも普通の変数のように直接代入してはいけません。React Nativeではそう言うルールです。
では早速WelcomeScreen
コンポーネントのstate
にnull
を初期値として設定しましょう。onStartButtonPress()
関数の直上にconstructor()
関数を作り、その中でstate
を初期化します↓。
class WelcomeScreen extends React.Component {
constructor(props) { // ← おまじないの入力 props
super(props); // ← おまじないの文 super(props);
// `state`の`isInitialized`を`null`に初期化
// `AsyncStorage`の'isInitialized'とはまた別物
this.state = {
isInitialized: null
};
}
onStartButtonPress = async () => {
await AsyncStorage.setItem('isInitialized', 'true');
this.props.navigation.navigate('main');
}
// ゴニョゴニョ…
}
constructor()
関数は一番最初に実行される関数のことで、この関数の中でstate
を初期化します。あ、props
を入力として入れてかつ最初にsuper(props);
と書いているのはある種のおまじないです。忘れずに付けましょう。
次はstate
のisInitialized
(AsyncStorageの 'isInitialized' とは別物です!)がnull
である間、つまりAsyncStorage
からの読み取りが終わるまでの間に「只今アプリ読み込み中です」感を出す画面を実装します。その前にnull
かどうかを判断する外部ツールを$ npm
でインストールします↓。
$ npm install lodash
lodash
のインストールが終わったら念のため再度、
$ npm install
をしておきます。ではWelcomeScreen.js
に戻り、先程入れたlodash
からアンダーバー_
をインポートします(名前が記号って不思議ですね)↓。
import _ from 'lodash';
import React from 'react';
import { StyleSheet, Text, View, ScrollView, Image, Dimensions, AsyncStorage } from 'react-native';
import { Button } from 'react-native-elements';
また、実はExpo XDEから作成したReact Nativeプロジェクトだと、expo
と言う名のグループ(正式に言うとライブラリ)から色々便利ツールをインポートできるという地味に強いメリットもあります。今回はAppLoading
と言うコンポーネントをexpo
からインポートして使いましょう↓。
import _ from 'lodash';
import React from 'react';
import { StyleSheet, Text, View, ScrollView, Image, Dimensions, AsyncStorage } from 'react-native';
import { Button } from 'react-native-elements';
import { AppLoading } from 'expo';
下準備が整ったところで、次は「state
のisInitialized
がnull
だったらAppLoading
を表示する」を実装しましょう。対象物がnull
かどうか判断する文章はlodash
の_
を使用して、
_.isNull(対象物) // ← 対象物が`null`なら`true`を、`null`じゃないなら`false`を返す
と書くことができます。これを応用してrender()
関数に「state
のisInitialized
がnull
だったらいち早くAppLoading
を描画する」と言う風に書きます↓。
render() {
if (_.isNull(this.state.isInitialized)) {
// もし`state`の`isInitialized`が`null`だったらいち早く`AppLoading`を描画
return <AppLoading />;
}
// もしそうじゃなかったら`ScrollView`を描画
return (
<ScrollView
horizontal
pagingEnabled
style={{ flex: 1 }}
>
{this.renderSlides()}
</ScrollView>
);
}
この状態で動作確認すると、
ずっとこの画面の状態で止まります。state
のisInitialized
がずっとnull
から変わらないため一向にWelcomeScreenへ進みません笑。これではまずいため、どっかのタイミングでAsyncStorage
を読み込んで→その情報を使ってstate
のisInitialized
をtrue
かfalse
に上書きしなければいけません。
そこで適任な関数が、componentWillMount()
です。これはconstructor()
関数の後、render()
関数の中にあるタグやタグやらのコンポーネント達が描画される前に実行される関数です。
constructor()
→ componentWillMount()
→ render()
って順番です。このcomponentWillMount()
の中で「AsyncStorage
から情報を読み取り→その情報を元にstate
のisInitialized
をtrue
かfalse
に上書きする」と言う実装をします↓。
constructor(props) {
// ゴニョゴニョ…
}
componentWillMount() {
// `AsyncStorage`の'isInitialized'から情報を読み込んで`isInitializedString`に保存
let isInitializedString = AsyncStorage.getItem('isInitialized');
// もし`AsyncStorage`の'isInitialized'から読み込んだ情報が'true'だったら
if (isInitializedString === 'true') {
// `state`の方の`isInitialized`に`true`と上書き
this.setState({ isInitialized: true });
// 'main'画面へ飛ばす
this.props.navigation.navigate('main');
// もし`AsyncStorage`の'isInitialized'から読み込んだ情報が'true'じゃなかったら
} else {
// `state`の方の`isInitialized`に`false`と上書き
this.setState({ isInitialized: false });
}
}
onStartButtonPress = async () => {
// ゴニョゴニョ…
}
内容を解説をすると、まずAsyncStorage
に保存できるデータ形式は文字形式のみなので、必然と読み取ったデータも文字形式となります。なのであえて格納先の変数をisInitializedString
という名前にしており(stringは文字列という意味)、後述しますがちょっと事情があってconst
ではなくlet
という接頭辞を使用しています。
// `AsyncStorage`の'isInitialized'から情報を読み込んで`isInitializedString`に保存
let isInitializedString = AsyncStorage.getItem('isInitialized');
もしisInitializedString
が(ただのtrue
ではなく文字の) 'true' であった場合はstate
の方のisInitialized
にtrue
と上書きしてから 'main' 画面へ飛ばし、
// もし`AsyncStorage`の'isInitialized'から読み込んだ情報が'true'だったら
if (isInitializedString === 'true') {
// `state`の方の`isInitialized`に`true`と上書き
this.setState({ isInitialized: true });
// 'main'画面へ飛ばす
this.props.navigation.navigate('main');
}
もしそうでなかった場合はstate
の方のisInitialized
にfalse
と上書きする
// もし`AsyncStorage`の'isInitialized'から読み込んだ情報が'true'じゃなかったら
} else {
// `state`の方の`isInitialized`に`false`と上書き
this.setState({ isInitialized: false });
}
という感じです。これでロジックは完成なのですが、ただAsyncStorage
から保存した情報を読み取るのも非同期処理なので、「処理が終わるまで待ってあげるよ」宣言を忘れずにしなければいけません。なのでasync
を関数(今回はcomponentWillMount()
)の先頭に、await
を実際に待機する文(今回はAsyncStorage.getItem()
)の先頭に付けます↓。
// ↓`await`を使う関数は文頭に`async`と書く必要がある
async componentWillMount() {
// `AsyncStorage`の処理を`await`(待機)してあげる
// `await`を使うために`const`ではなく`let`にした
let isInitializedString = await AsyncStorage.getItem('isInitialized');
if (isInitializedString === 'true') {
this.setState({ isInitialized: true });
this.props.navigation.navigate('main');
} else {
this.setState({ isInitialized: false });
}
}
await
を介して変数に代入するために、isInitializedString
変数の接頭辞はconst
ではなくlet
にしたのです。
これでWelcomeScreen.js
は完成したので、動作確認してみましょう。
今度は、command⌘+r
で更新したらちゃんとウェルカム画面が表示されるようになりましたね。
①constructor()
でstate
の方のisInitialized
がnull
に初期化される
↓
②componentWillMount()
でAsyncStorage
の方の 'isInitialized' が 'true' かどうか読み込み始める
↓
③render()
が実行されるが、state
の方のisInitialized
はまだnull
なので、一旦が描画される
↓
④componentWillMount()
でのAsyncStorage
の読み込みが完了し、state
の方のisInitialized
がtrue
に上書きされる
↓
⑤state
の値に変更がある度に再度render()
が実行されるというルールがあるため、今度はが描画される
という流れです。いくらAsyncStorage
の読み書きには少し時間を要するとは言え、最近のスマホは高スペックなので実際②〜④は目に見えないほど一瞬で終わります。この流れは、試しにrender()
関数でではなく真っピンクのを描画するように変更してみるともっとわかりやすくなります。
return <AppLoading />;
↓
//return <AppLoading />;
return <View style={{ flex: 1, backgroundColor: 'pink' }} />;
一瞬だけ真っピンクの画面が現れますね。これが上記の②〜④の間、つまりstate
の方のisInitialized
がnull
である間のことですね。componentWillMount()
とasync
/await
の動きが掴めたら、ちゃんとrender()
関数を元に戻しておきましょう笑。
//return <AppLoading />;
return <View style={{ flex: 1, backgroundColor: 'pink' }} />;
↓
return <AppLoading />;
これでWelcomeScreen.js
は完成です!上図の通りウェルカム画面の最終ページのスタートボタンを押すと、もうそれ以降いくらcommand⌘+r
で更新してもウェルカム画面は出てこなくなります(最終的には練習のためにウェルカム画面を復活させるボタンも作りますのでご心配なく笑)。では次章から本題のホーム画面へ入っていきましょう。次章ではなんとあのReact Native第一の難関、Reduxが遂に登場します……でも大丈夫です、図解でわかりやすく解説していきますのでご安心ください ;->
ホーム画面の見た目を作ろう
Reduxとかいう魔のフレームワークの前に、まずはホーム画面HomeScreen.js
の見た目から作りましょう。メルカリなどのアプリでもよく見かける、評価一覧を「すべて / 良い / 普通 / 悪い」でグループ分けできるボタンを作成します。
この機能は、既にインストール済みのreact-native-elements
の中にButtonGroup
という名前で含まれています。早速HomeScreen.js
にて(WelcomScreen.js
じゃないですよ!)インポートしましょう↓。
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { ButtonGroup } from 'react-native-elements';
// ↑コレ
class HomeScreen extends React.Component {
// ゴニョゴニョ…
インポートしたら、render()
関数内を書き換えてを表示させましょう。
class HomeScreen extends React.Component {
render() {
return (
<View style={{ flex: 1, justifyContent: 'center' }}>
<Text>This is HomeScreen</Text>
</View>
);
}
}
↓
class HomeScreen extends React.Component {
render() {
const buttonList = [
'All',
'Great (0)',
'Good (0)',
'Poor (0)',
];
return (
<View style={{ flex: 1 }}>
<ButtonGroup
buttons={buttonList}
/>
</View>
);
}
}
は、buttonsプロパティに表示したいボタンのリスト(今回は評価ランクの「すべて / 良い / 普通 / 悪い」)を配列で渡すと、横一列にボタン群を綺麗に表示してくれます。なので、buttonsプロパティに配列のbuttonList
を渡しており、buttonList
の配列の中身は「すべて / 良い / 普通 / 悪い」の英語訳に当たる「All / Great / Good / Poor」を入れています。ここで注意は、buttonsプロパティはHTML風(正確にはJSXと呼ぶ)なのに対してbuttonList
はJavaScriptなので、ちゃんと波括弧{ }
で囲んで「今からJavaScript書くよ〜」と示すのを忘れない事です。波括弧{ }
を忘れると真っ赤なエラーに見舞われます汗。また「Great / Good / Poor」の後ろについている括弧付き数字は、各評価の数を表していますが、今はとりあえずゼロにしておきます。
この状態で動作確認してみましょう。
ちゃんと表示されていますね!しかしこのままでは味気ないので、ギミックを付け加えていきましょう。現段階では、最初は All も Great も Good も Poor もどれも選択されていませんが、普通は大体最初から All が選択されてますよね。このギミックをstate
と、タグのselectedIndexプロパティを使って実装します↓。
class HomeScreen extends React.Component {
constructor(props) { // ← おまじないの入力 props
super(props); // ← おまじないの文 super(props);
this.state = {
selectedIndex: 0,
};
}
render() {
const buttonList = [
'All',
'Great (0)',
'Good (0)',
'Poor (0)',
];
return (
<View style={{ flex: 1 }}>
<ButtonGroup
buttons={buttonList}
selectedIndex={this.state.selectedIndex}
/>
</View>
);
}
}
まずはconstructor()
関数でstate
のselectedIndex
を0
に初期化し、その値this.state.selectedIndex
をの方のselectedIndexプロパティにぶち込みます。そうすると、
最初から All ボタンが選択された状態でアプリが起動する様になります。タグでは左のボタンから順に、0番から番号が付いています(配列と同じく先頭番号は0番です)ので、state
のselectedIndex
が
0
だとAll|1
だとGreat|2
だとGood|3
だとPoor
に割り当てられます。
しかし現状では、 Great / Good / Poor のどのボタンを押しても一瞬光るだけで All から切り替わってくれません(試しに各ボタンを押してみてください)。なので次は、 Great / Good / Poor の各ボタンが押されたら、All からそのボタンに切り替わるというギミックを実装します。以前使用したタグと同様に、タグでもonPressプロパティを使用します↓。
onButtonGroupPress = (selectedIndex) => {
this.setState({
selectedIndex: selectedIndex
// selectedIndex: selectedIndex → selectedIndex と省略しても可
});
}
render() {
const buttonList = [
'All',
'Great (0)',
'Good (0)',
'Poor (0)',
];
return (
<View style={{ flex: 1 }}>
<ButtonGroup
buttons={buttonList}
selectedIndex={this.state.selectedIndex}
onPress={this.onButtonGroupPress}
/>
</View>
);
}
① 各ボタンが押されるたびにonPressプロパティに設定されている関数this.onButtonGroupPress
(に紐付けられているアロー関数)が発動します。
↓
② その際にonPressプロパティから「今押されたボタンの番号(0~3)」が入力として入ってくるので、すかさずstate
のselectedIndex
をその入力そのままの値で更新します。
↓
③ すると連動してタグの方のselectedIndexプロパティも更新されるので、アプリ画面上のボタンが切り替わります。
という流れです。では動作確認してみましょう。
ちゃんとボタンを押すたびに切り替えられてますね。では次は仮の評価データ達を作り、それらをリスト表示する実装をしましょう。
仮のデータ達は、JavaScript オブジェクト(波括弧{ }
)の配列(大括弧[ ]
)で表します。
const 仮のデータ達 = [
{ 項目A: 値1-1, 項目B: 値1-2, 項目C: 値1-3 },
{ 項目A: 値2-1, 項目B: 値2-2, 項目C: 値2-3 },
{ 項目A: 値3-1, 項目B: 値3-2, 項目C: 値3-3 },
.
.
.
];
こんな感じの構造の配列を、allReviewsTmp
という名でインポート文とclass定義文の間に書きます↓。
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { ButtonGroup } from 'react-native-elements';
const GREAT = 'sentiment-very-satisfied';
const GOOD = 'sentiment-satisfied';
const POOR = 'sentiment-dissatisfied';
const allReviewsTmp = [
{
country: 'USA',
dateFrom: 'Jan/15/2018',
dateTo: 'Jan/25/2018',
imageURIs: [
require('../assets/add_image_placeholder.png'),
require('../assets/add_image_placeholder.png'),
require('../assets/add_image_placeholder.png'),
],
rank: GREAT,
},
{
country: 'USA',
dateFrom: 'Feb/15/2018',
dateTo: 'Feb/25/2018',
imageURIs: [
require('../assets/add_image_placeholder.png'),
require('../assets/add_image_placeholder.png'),
require('../assets/add_image_placeholder.png'),
],
rank: GOOD,
},
{
country: 'USA',
dateFrom: 'Mar/15/2018',
dateTo: 'Mar/25/2018',
imageURIs: [
require('../assets/add_image_placeholder.png'),
require('../assets/add_image_placeholder.png'),
require('../assets/add_image_placeholder.png'),
],
rank: POOR,
},
];
class HomeScreen extends React.Component {
// ゴニョゴニョ…
各データに必要な項目の内訳は、
・country
: 国名
・dateFrom
: 旅行開始日
・dateTo
: 旅行終了日
・imageURIs
: 画像の保存場所(計3枚なので更に配列にしている)
・rank
: 旅行の評価
imageURIs
は、3枚の画像をひとまとめにするために更に配列を使用しています。add_image_placeholder.png はこちらからダウンロードしてください。またrank
は今後複数箇所で使用するため、新たにGREAT
, GOOD
, POOR
という変数を作りました。
これで仮データの用意は終わったので、データをリスト表示する部分を作っていきましょう。まずはrender()
関数内にrenderReviews()
というリスト表示専用の関数を新たに用意しましょう↓。
renderReviews() { // ← 追記部分
return (
);
}
onButtonGroupPress = (selectedIndex) => {
// ゴニョゴニョ…
}
render() {
// ゴニョゴニョ…
return (
<View style={{ flex: 1 }}>
<ButtonGroup
buttons={buttonList}
selectedIndex={this.state.selectedIndex}
onPress={this.onButtonGroupPress}
/>
{this.renderReviews()} // ← 追記部分
</View>
);
}
これからrenderReviews()
関数の中身を書いていくのですがその前に、
・ScrollView
をreact-native
から
・ListItem
をreact-native-elements
から
インポートしてください。
import { StyleSheet, Text, View, ScrollView } from 'react-native';
// ↑コレ
import { ButtonGroup, ListItem } from 'react-native-elements';
// ↑コレ
前回のウェルカム画面と同じ様に、の中で配列の個数分だけ今度はじゃなくてを描画する、という作戦です。
では評価データ達を、評価ランクごとに仕分けするロジックを作っていきましょう。ALL / GREAT / GOOD / POORの中でどのボタンが今押されているかを表すthis.state.selectedIndex
が、
・0
だったらAllなので特に何もしない
・1
だったらGREATなのでGREAT
を
・2
だったらGOODなのでGOOD
を
・3
だったらPOORなのでPOOR
を
格納する様な変数をまずは用意します。上記の様な複数パターンに場合分けする時は if 文を何回も書くよりも switch 文の方が向いてます。
switch (比較したい対象) {
case 条件1: // 条件1に当てはまるなら
/*
条件1の時にしたいこと
*/
break; // 比較を終了して抜け出す
case 条件2: // 条件2に当てはまるなら
/*
条件2の時にしたいこと
*/
break; // 比較を終了して抜け出す
case 条件3: // 条件3に当てはまるなら
/*
条件3の時にしたいこと
*/
break; // 比較を終了して抜け出す
.
.
.
default: // どの条件にも当てはまらなかったら
/*
どの条件にも当てはまらない時にしたいこと
*/
break; // 比較を終了して抜け出す
}
場合によって変数の中身が変わり得る時は、接頭辞としてconst
ではなくlet
を使います↓。
renderReviews() {
let reviewRank; // まずは`GREAT`,` GOOD`,` POOR`を格納する変数を用意
switch (this.state.selectedIndex) { // もし`this.state.selectedIndex`が、
case 1: // `1`だったら
reviewRank = GREAT; // `GREAT`を代入
break; // 比較を終了して抜け出す
case 2: // `2`だったら
reviewRank = GOOD; // `GOOD`を代入
break; // 比較を終了して抜け出す
case 3: // `3`だったら
reviewRank = POOR; // `POOR`を代入
break; // 比較を終了して抜け出す
default: // どの条件にも当てはまらなかったら
break; // (特に何もせず)抜け出す
}
return (
);
}
これでreviewRank
には押されたボタン( GREAT / GOOD / POOR )に応じて表示すべきランクの種類が格納される様になります。
ここでプログラミング的注意があります。今この瞬間でしたら私達は1~3という数字がGREAT ~ POORを表しているとわかりますが、数ヶ月後にもう一度コードを見た時にはこの1~3という数字が一体何のことを意味しているのか覚えていないかもしれません。もしくは他人の同僚が見た時にパッと見で数字の意味が伝わりづらいです。ひょっとしたら周辺のコードをじっくり読み返せばわかるかもしれませんが、それはそれでだるいので、こういった意味を持つ数字にはちゃんと名前を付けてあげましょう ↓。
const ALL_INDEX = 0; // ← 追記部分
const GREAT = 'sentiment-very-satisfied';
const GREAT_INDEX = 1; // ← 追記部分
const GOOD = 'sentiment-satisfied';
const GOOD_INDEX = 2; // ← 追記部分
const POOR = 'sentiment-dissatisfied';
const POOR_INDEX = 3; // ← 追記部分
const allReviewsTmp = [
// ゴニョゴニョ…
];
class HomeScreen extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedIndex: ALL_INDEX, // ← 変更部分
};
}
renderReviews() {
let reviewRank;
switch (this.state.selectedIndex) {
case GREAT_INDEX: // ← 変更部分
reviewRank = GREAT;
break;
case GOOD_INDEX: // ← 変更部分
reviewRank = GOOD;
break;
case POOR_INDEX: // ← 変更部分
reviewRank = POOR;
break;
default:
break;
}
return (
);
}
// ゴニョゴニョ…
}
ついでにconstructor()
関数内でのstate
の初期化に使っていた0
もALL_INDEX
に置き換えました。これで、「ん〜と0, 1, 2, 3ってどういう意味だったっけ…?」と迷うことが無くなります。
話を戻しましょう。次は、「仮データの中から特定の評価のデータだけ抜き取る」という作業を実装します。より具体的には、「仮データを1個ずつ見て行って特定の評価(GREAT, GOOD, POORのどれか)と一致してる奴だけを新たな配列に順次追加していく」ということをします。今回のように同じ処理を繰り返し行うといった時は for 文という構文を使うと便利です↓。
for (どこから繰り返すか; いつまで繰り返すか; 何ステップずつ繰り返すか) {
// 繰り返したい処理
}
より具体的には、
for (let i = 0; i < 10; i++) {
// 繰り返したい処理
}
と書くと、i
が0から1ずつ増えていって9になるまでの計10回分繰り返してくれます。i
が0から10までじゃないことに気をつけて下さい(計11回分になってしまうため)。ちなみにi++
はi = i + 1
の略で、「今のi
の値に1を足す」という意味です。したがって、
for (let i = 1; i <= 10; i++) {
// 繰り返したい処理
}
こう書いても確かにi
が1から1ずつ増えていって10になるまでの計10回分繰り返してくれますが、プログラミングの世界では先頭番号が1からではなく0から始まるのがお約束なので、
for (let i = 0; i < 10; i++) {
// 繰り返したい処理
}
こう書く方が好まれます。さて、「仮データを1個ずつ見て行って特定の評価(GREAT, GOOD, POORのどれか)と一致してる奴だけを新たな配列に順次追加していく」をするためには、仮データ配列の中に何個データが入ってるか知らなければいけません。配列の要素数、つまり配列の長さは配列名.length
と書くと取得できますので、今回使うfor文は
// `i` が0から1ずつ増えていって(`allReviewsTmp.length`-1)になるまでの
// 計`allReviewsTmp.length`回分繰り返す
for (let i = 0; i < allReviewsTmp.length; i++) {
// 繰り返したい処理
}
となります。繰り返したい処理は「もしi
番目の仮データの評価がreviewRank
の中に入ってる評価(GREAT, GOOD, POORのどれか)と一致していたら新たな配列にぶち込む」です。また配列に要素を足すには.push()
関数が使えます。新要素を配列の中に後ろから押し込む感じなので、プッシュ関数という名前です↓。
// 新たな空の配列を用意
let rankedReviews = [];
for (let i = 0; i < allReviewsTmp.length; i++) {
// もし`i`番目の仮データ`allReviewsTmp[i]`の`rank`項目が`reviewRank`と一致したら、
if (allReviewsTmp[i].rank === reviewRank) {
// さっき用意した新たな配列にぶち込む
rankedReviews.push(allReviewsTmp[i]);
}
}
これで、新たに用意した配列rankedReviews
に必要な分だけのデータを抜き取ることができました。ただこれでは、ALLボタンだった時にどうするか書かれていません。ALLボタンの時(this.state.selectedIndex
がALL_INDEX
の時)は当然データを全部表示するので、rankedReviews
にallReviewsTmp
を丸ごとコピーします↓。
let rankedReviews = [];
// もし`this.state.selectedIndex`が`ALL_INDEX`だったら、
if (this.state.selectedIndex === ALL_INDEX) { // ←追記部分
// 丸ごとコピー
rankedReviews = allReviewsTmp; // ←追記部分
// もしそうじゃなかったら、
} else { // ←追記部分
// 繰り返し処理
for (let i = 0; i < allReviewsTmp.length; i++) {
if (allReviewsTmp[i].rank === reviewRank) {
rankedReviews.push(allReviewsTmp[i]);
}
}
} // ←追記部分
これで評価別に分けたデータが完成したので、次は実際に描画するところを実装します。配列の個数分だけ同じ描画を繰り返すのに便利なのは前回と同じく、.map()
関数です。先程のfor文との違いは、繰り返し"処理"なのか、繰り返し"描画"なのかで(私は)使い分けています。何かコンポーネントを描画するということは必ずreturn ()
文があるので、「returnするなら.map()
関数、returnしないならfor文」ということになります。
今回.map()
関数で繰り返すことは、「の中でrankedReviews
配列の個数分だけを描画する」です↓。
renderReviews() {
// ゴニョゴニョ…
return (
<ScrollView>
{rankedReviews.map((review, index) => {
return (
<ListItem />
);
})
}
</ScrollView>
);
}
screens/HomeScreen.jsrenderReviews() {
// ゴニョゴニョ…
return (
<ScrollView>
{rankedReviews.map((review, index) => {
return (
<ListItem />
);
})
}
</ScrollView>
);
}
今回は.map()
関数(に渡されてるアロー関数)にreview
とindex
を渡しています。
-
rankedReviews
配列の各要素の一つ…review
- 現要素の番号...
index
まずはrank
によって描画するアイコン色を変えるようにしましょう。ここでもまた switch 文が出てきます↓。
renderReviews() {
// ゴニョゴニョ…
return (
<ScrollView>
{rankedReviews.map((review, index) => {
let reviewColor; // 新たな変数を`let`で用意(場合によって値が変わるため)
switch (review.rank) { // もし`review`の中の`rank`項目が、
case GREAT: // `GREAT`だったら、
reviewColor = 'red'; // 赤を指定
break; // 比較を終了して抜け出す
case GOOD: // `GOOD`だったら、
reviewColor = 'orange'; // オレンジを指定
break; // 比較を終了して抜け出す
case POOR: // `POOR`だったら、
reviewColor = 'blue'; // 青を指定
break; // 比較を終了して抜け出す
default: // どの条件にも当てはまらなかったら
break; // (特に何もせず)抜け出す
}
return (
<ListItem />
);
})
}
</ScrollView>
);
}
またここでも今後のことを考えて'red', 'orange', 'blue'をそのまま書くのではなく、変数に置き換えましょう↓。
const ALL_INDEX = 0;
const GREAT = 'sentiment-very-satisfied';
const GREAT_COLOR = 'red'; // ← 追記部分
const GREAT_INDEX = 1;
const GOOD = 'sentiment-satisfied';
const GOOD_COLOR = 'orange'; // ← 追記部分
const GOOD_INDEX = 2;
const POOR = 'sentiment-dissatisfied';
const POOR_COLOR = 'blue'; // ← 追記部分
const POOR_INDEX = 3;
const allReviewsTmp = [
// ゴニョゴニョ…
];
class HomeScreen extends React.Component {
// ゴニョゴニョ…
renderReviews() {
// ゴニョゴニョ…
return (
<ScrollView>
{rankedReviews.map((review, index) => {
let reviewColor;
switch (review.rank) {
case GREAT:
reviewColor = GREAT_COLOR; // ← 変更部分
break;
case GOOD:
reviewColor = GOOD_COLOR; // ← 変更部分
break;
case POOR:
reviewColor = POOR_COLOR; // ← 変更部分
break;
default:
break;
}
return (
<ListItem />
);
})
}
</ScrollView>
);
}
// ゴニョゴニョ…
}
これで評価別のアイコン色の指定は完了しました。次はの中身を書いていきます。タグには、以下のプロパティを与えます。
・key
…他と被らない一意の数。大体の場合はindex
をそのまま使用
・leftIcon
…左側のアイコン。JavaScriptオブジェクトで指定
・title
…メインタイトル。文字色は黒
・subtitle
…サブタイトル。文字色は灰色
return (
<ScrollView>
{rankedReviews.map((review, index) => {
// ゴニョゴニョ…
return (
<ListItem
key={index}
leftIcon={{ name: review.rank, color: reviewColor }}
title={review.country}
subtitle={`${review.dateFrom} ~ ${review.dateTo}`}
/>
);
})
}
</ScrollView>
);
特殊なのはleftIcon
とsubtitle
ですね。leftIcon
はJavaScriptオブジェクトで、
・name
...アイコンの名前。'sentiment-very-satisfied' など
・color
...アイコンの色。'red' など
の2項目を指定します。これら2項目は、HomeScreen.js
の上の方にconst GREAT
とかconst GREAT_COLOR
とかって大文字の名前で定義し直したやつですね。
subtitle
は変数の数値をテキストに変換するバッククォート機能(正式名はテンプレート文字列)を使います。テキストは普通シングルクォートで囲むのですが、バッククォートで囲んだ中で${変数名}
と入れると、そこの部分のテキストだけ変数の数値が表示されるようになります。シングルクォートの中で '${変数名}' とやっても何も起きません、そのまま文字通り表示されます笑。
いやーここまで長かったですね!やっと動作確認できるとこまで来ました!
ちゃんと Great / Good / Poor 別に分かれていますね。因みにタグのsubtitle
プロパティでのテンプレート文字列について、バッククォートとシングルクォートの違いを比較した画像がこちらです↓。
バッククォートで囲んだ方は変数の値がちゃんと反映されているのに対し、シングルクォートで囲んだ方はそのまま文字通り表示されちゃってます。
では次にこのタグ(=レビューデータ)を押したらDetailScreen.js
に飛ぶように追加しましょう。ここで使うのはonPress
プロパティです↓。
return (
<ListItem
key={index}
leftIcon={{ name: review.rank, color: reviewColor }}
title={review.country}
subtitle={`${review.dateFrom} ~ ${review.dateTo}`}
onPress={() => this.onListItemPress(review)} // ←追記部分
/>
);
後々使うので、onPress
が発動してonListItemPress()
関数が呼ばれる際にreview
(今押されたレビューデータ丸ごと)を引数として渡します。
そしてrenderReviews()
関数の直上に新たにonListItemPress()
関数を作り、onPress
プロパティから渡された引数はselectedReview
という名で受け止めて一旦放置します。関数の中身は、this.props.navigation.navigate()
を使用して 'detail' に画面遷移するように書きます↓。
constructor(props) {
// ゴニョゴニョ…
}
// `onPress`からの引数は`selectedReview`という名で受け止める(一旦放置。後で使用)
onListItemPress = (selectedReview) => {
// 'detail'に飛ぶ
this.props.navigation.navigate('detail');
}
renderReviews() {
// ゴニョゴニョ…
}
では動作確認しましょう。各レビューデータをクリックしてみて下さい。
だいぶ形になって来ましたね。最後に Great / Good / Poor の末尾にある数字の (0) の所を、データの個数を表すように変えましょう。これもこれまでにやって来たのと同じように、switch文でGREATかGOODかPOORかを判定するのをfor文でひたすら繰り返すだけです。render()
関数の中にfor文とswitch文を追加し、buttonList
も先程出てきたバッククォートによるテンプレート文字列に変更します↓。
render() {
let nGreat = 0; // "Number of Great" の略。値が変更され得るので`let`で宣言
let nGood = 0; // "Number of Good" の略。値が変更され得るので`let`で宣言
let nPoor = 0; // "Number of Poor" の略。値が変更され得るので`let`で宣言
// `i` が0から1ずつ増えていって(`allReviewsTmp.length`-1)になるまでの
// 計`allReviewsTmp.length`回分繰り返す
for (let i = 0; i < allReviewsTmp.length; i++) {
switch (allReviewsTmp[i].rank) { // もし`allReviewsTmp[i]`の`rank`が、
case GREAT: // `GREAT`だったら、
nGreat++; // `nGreat`を1追加
break; // 比較を終了して抜け出す
case GOOD: // `GOOD`だったら、
nGood++; // `nGood`を1追加
break; // 比較を終了して抜け出す
case POOR: // `POOR`だったら、
nPoor++; // `nPoor`を1追加
break; // 比較を終了して抜け出す
default: // それ以外だったら、
break; // (特に何もせず)抜け出す
}
}
const buttonList = [
`All (${allReviewsTmp.length})`, // ←バッククォート&テンプレート文字列に変更
`Great (${nGreat})`, // ←バッククォート&テンプレート文字列に変更
`Good (${nGood})`, // ←バッククォート&テンプレート文字列に変更
`Poor (${nPoor})` // ←バッククォート&テンプレート文字列に変更
];
return(
// ゴニョゴニョ…
);
}
Allの時はallReviewsTmp
配列にあるデータ数をそのまま出せば良いので、allReviewsTmp.length
でOKです。それでは動作確認してみましょう。
ちゃんとデータの個数が反映されていますね!では次章からついにあの魔の Redux が始まります…。
Reduxとは
初回起動かどうか」や「今All / Great / Good / Poorのどのボタンが押されているか」などといったアプリの”状態”を表すには、今までthis.state
という物を使って表現してきました。しかしthis.state
は同じファイルの中でしか有効ではないため、例えばHomeScreen.js
のthis.state
はHomeScreen.js
内からしかアクセスできず、他のDetailScreen.js
などからは見れません。したがってファイル間( ≒コンポーネント間)をまたがってアプリ全体で情報を共有したい時、特に画面遷移を伴う時はちょっと違った手を使わねばなりません。そこで登場するのがこのReduxというシステムです。イメージとしては、データ保存場所が別で用意されており(正式名はStore)、どのページからもそこに保存されている共通データにアクセスできるという感じです。
ただ、この共通データ保存場所であるStoreにデータを保存するまでがまわりくどいのなんの……。とりあえずまずは、「Storeの共通データへどのページからもアクセスできるようにする」を実装しましょう。React NativeでReduxを使うのに必要な3つのものを$ npm
コマンドでインストールし、
この続きはnote.muにて
(残り55,057文字 / 画像33枚)