30
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【両OS対応】 React Nativeで爆速モックアップ/プロトタイプアプリを作ろう 2/3 【ホーム画面編】

Last updated at Posted at 2018-06-03

↑このシリーズの最終完成形

picture_pc_0f8119a3d9f85fe1210ff18da947274a.gif

↑前回記事までの目標

19.gif

↑この記事での目標

この記事で得れる物

・上図のスマホアプリが作れるようになる
・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. プラスボタン編

画面遷移を付けよう

まずはモバイルアプリの基本である、画面下部のタブによる画面遷移から始めましょう。イメージはこんな感じです。

2.jpg

前回作ったWelcomeScreenから → メインである
HomeScreen / AddScreen / ProfileScreenの3画面に飛んだ後は、再度WelcomeScreenに戻らせないためにあえてタブを隠して行き来不可にします。一方で、(当然ですが)メインの3画面間はタブを出して行き来可能にさせます。本記事ではこのHomeScreenを完成させます。

幸運な事にReact Nativeには画面遷移の実装を超楽にしてくれるreact-navigationと言うものがあるので、前回記事と同じようにターミナル
$ npmコマンドを使って外部からインストールしましょう。
(2018/5/7にreact-navigationがver. 2へアプデで大幅仕様変更されましたが、本noteでは早速ver. 2へ対応しております)

1.jpg

$ npm install react-navigation

んで$ npmした時のお約束で、念のため再度$ npm installします。

$ npm install

これで画面遷移を作る準備ができました。早速App.jsに行き先ほどインストールしたreact-navigationからcreateBottomTabNavigatorをインポートします。ついでにHomeScreen / AddScreen / ProfileScreenのメイン3画面もインポートしちゃいましょう(中身は後で作ります)。

App.js
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()関数を使ってタブ遷移の詳細を追記して行きます。

App.js
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が入れ子になっている事です(プログラミングではこういう事よくあります)。

picture_pc_0bd5169b390b4dab7797f26803c11c02.jpg

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を作成し、

4.png

以下の雛形をコピペしていきましょう↓。

screens/HomeScreen.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;
screens/AddScreen.js
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;
screens/ProfileScreen.js
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のDeviceOpen on iOS simulatorをクリックします。

1.gif

おっと、1つ目の(=親の)BottomTabNavigatorつまりwelcomemainのタブをまだ非表示にしてなかったので、タブの上に更にタブが乗っかっちゃってますね汗。ここで親BottomTabNavigatorのタブを非表示にする前に、タブ遷移文の構文をスッキリさせるために新たにMainTabという変数を作り、子BottomTabNavigatorの中身をその中にぶち込みます。

App.js
const MainTab = createBottomTabNavigator({
  homeStack: { screen: HomeScreen },
  addStack: { screen: AddScreen },
  profileStack: { screen: ProfileScreen }
});

const NavigatorTab = createBottomTabNavigator({
  welcome: { screen: WelcomeScreen },
  main: { screen: MainTab }
});

5.png

この時点では書き方をスッキリさせたたけですので、アプリの挙動自体は何ら変わっていません。次に(やっと)親BottomTabNavigatorのタブを非表示にします。そのためには、

createBottomTabNavigator({ 画面達 }, { タブに関する設定など });

↑こんな感じでcreateBottomTabNavigator()関数の2つ目の波括弧{ }内にタブの設定を書き込んでいきます。タブを非表示にする設定は、navigationOptions: { tabBarVisible: false } です↓。

App.js
const MainTab = createBottomTabNavigator({
  homeStack: { screen: HomeScreen },
  addStack: { screen: AddScreen },
  profileStack: { screen: ProfileScreen }
});

const NavigatorTab = createBottomTabNavigator({
  welcome: { screen: WelcomeScreen },
  main: { screen: MainTab }
}, {
  navigationOptions: { tabBarVisible: false } // ←追記部分
});

こうすることでwelcomemainのタブを非表示にできます。また、そうするとWelcomeScreenからメインタブに飛ぶこともできなくなっちゃうので、screens/WelcomeScreen.jsのボタンをいじります。より具体的には前回作ったonStartButtonPress()関数を「アラートを出す」から→「メインタブに飛ばす」に変えます。

screens/WelcomeScreen.js
class WelcomeScreen extends React.Component {
  onStartButtonPress = () => {
    Alert.alert(
      'Alert',
      'The button was pressed',
      [
        { text: 'OK' },
      ],
      { cancelable: false }
    );
  }

  // ゴニョゴニョ…

}

変更前「アラートを出す」

screens/WelcomeScreen.js
class WelcomeScreen extends React.Component {
  onStartButtonPress = () => {
    this.props.navigation.navigate('main');
  }

  // ゴニョゴニョ…

}

変更後「メインタブに飛ばす」

指定の画面に遷移する魔法の文は、this.props.navigation.navigate('指定ID') です。thisは前回説明した通り「同じ屋根の下」(= ここではWelcomeScreenコンポーネント)という意味を表していますが、問題はpropsnavigationです。特にpropsは厄介者で、後に登場するもう1つの曲者stateとの比較で記事1本書けちゃうほどなので、ここでは深く立ち入りません。とにかく、指定の画面に遷移する時は例外を除いてほとんどthis.props.navigation.navigate('指定ID') で済みます。

ではこの状態で動作確認してみましょう!

2.gif

この調子で画面遷移を全て完成させちゃいましょう!ここでMainTabの各タブにStackNavigatorを追加します。StackNavigatorとはよくあるあの、画面が奥にどんどん突き進んで行って、左上の”戻る”ボタン or 画面上を左から右へヌルッとスライドする事によって1個前の画面に戻れるやつです。アプリの設定画面などでよくあるやつです。

6.jpg

またStackNavigator中の特定の画面では、途中で他のタブへ移動させないようにするために、タブを隠すよう設定します。ではcreateStackNavigator()関数を使って実際に作っていきましょう。まずはcreateBottomTabNavigator()関数の時と同じく、createStackNavigator()関数をreact-navigationからインポートし、新たに出てきたDetailScreen / Setting1Screen / Setting2Screenもインポートします(中身は後で書きます)。

App.js
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に変更してます↓。

App.js
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 } // ←変更部分
});

7.png

StackNavigatorの下準備が整ったところで、動作確認のために

  1. HomeScreenDetailScreenを行き来するボタン
  2. AddScreenHomeScreenに戻るボタン(一方通行)
  3. ProfileScreenSetting1ScreenSetting2Screenを行き来するボタン

を付けましょう。screens/HomeScreen.jsには前回インストールしたreact-native-navigationButtonを付けます。んでonPressプロパティに「ボタンを押されたら'detail'に飛ぶ」ようにアロー関数( ) => { }を使って指示します。ただしアロー関数で指示したい内容がたった1文(今回はthis.props.navigation.navigate('detail') のみ)だけの場合は、( ) => { }の後ろの波括弧{ }は略しても良いというルールがあります↓。

screens/HomeScreen.js
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は新規作成します。特にボタンとかはまだ付けないです↓。

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-navigationIconを付けます。バツ印を表現するために、nameプロパティには"close"を選びました。こちらのonPressプロパティには「ボタンを押されたら'home'に飛ぶ」ようにアロー関数( ) => { }を使って指示します↓。

screens/AddScreen.js
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-navigationButtonを付けます。んでonPressプロパティに「ボタンを押されたら'setting1'に飛ぶ」ようにアロー関数( ) => { }を使って指示します↓。

screens/ProfileScreen.js
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'に飛ぶ」ようにアロー関数( ) => { }を使って指示します↓。

screens/Setting1Screen.js
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は新規作成します。特にボタンとかはまだ付けないです↓。

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

です。ではスペルミスがないかチェックして動作確認してみましょう!

3.gif

2階層目以降の画面からは、左上の”戻る”ボタン(今は見えないですけど) or 画面上を左から右へヌルッとスライドする事によって1個前の画面に戻れる事が確認できますね。本当は左上に "<" こんな感じのバック矢印ボタンが表示されるはずなんですけど、iOSシミュレーターが遅すぎて表示されません泣(実機テストならすぐ描画されると思います!)。またAddScreenは特別で、AddStackの中にAddScreenの1画面しかないため、戻るもくそもありません。

次に任意の画面でタブを隠す設定を書き込みましょう。イメージ図をもう一度確認すると、

6.jpg

HomeStackDetailScreen(=2階層目)以降
AddStackAddScreen(=1階層目)以降
ProfileStackSetting1Screen(=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階層目を表す

です。長かったですが結局答えはこちらです↓。

App.js
ProfileStack.navigationOptions = ({ navigation }) => {
  return {
    tabBarVisible: (navigation.state.index === 0)
  };
};

8.png

こうすることで、2階層目(navigation.state.indexの値が1)以降はタブが非表示になります。

同じことをHomeStackAddStackにも行いましょう。ただし注意はAddStackは1階層目(navigation.state.indexの値が0)からタブを隠すので、その一個前の数字ということで-1を指定しています。-1(=0階層目)なんて存在しないけど。

App.js
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>
    );
  }
}

これで動作確認してみましょう!

4.gif

ちゃんと希望の階層以降ではタブが隠れてますね!こうする事によって例えば、AddScreenでの入力作業を終える前に別のタブへ移動しちゃう事を防ぐ、とかができます。

これで画面遷移自体の大枠はできたので、次はこの味気ないヘッダーやタブ達を装飾していきます。ややこしいんですけど、ここでもまたnavigationOptionsが出てくるんですよね汗。

まず始めにPlatformというのをreact-nativeからインポートします。これは、アプリを開いてるのがiOSなのかAndroidなのか教えてくれるヤツです。

import { StyleSheet, Text, View, Platform } from 'react-native';

次にヘッダーに共通する点をひとまとめのJavaScript オブジェクトにしてheaderNavigationOprionsという名の変数に格納します。

App.js
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に適応してみましょう。

App.js
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: { }内に展開されます。こんな感じに↓。

App.js
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: …一個奥の画面から戻る際の、左上の戻るボタンの文字

です。動作確認してみると…

9.png

おお、綺麗な水色のヘッダーが表示されました!ヘッダーができると一気にアプリ感が増しますね。次はHomeStackの中のDetailScreenに適応してみましょう。

App.js
const HomeStack = createStackNavigator({
  home: {
    screen: HomeScreen,
    navigationOptions: {
      ...headerNavigationOptions,
      headerTitle: 'Treco',
      headerBackTitle: 'Home'
    },
  },
  detail: {
    screen: DetailScreen,
    navigationOptions: {
      ...headerNavigationOptions,
      headerTitle: 'Detail',
    }
  }
});

HomeScreenと違ってDetailScreenはもうこれ以上奥に画面遷移しない予定なので、headerBackTitle: は要りません。こんな感じになるはずです↓。

10.png

iOSシミュレーターは実機に比べて処理速度遅いので、もしかしたら画面左上の戻るボタンの矢印マーク" < "は出てくるのが遅いかもしれません。数秒待つと描画されます(矢印マークが出てなくてもボタン自体は押せます)。

ちょっとAddStackは飛ばして、次はProfileStackのスクリーン達にnavigationOprions: { }を同じ要領で追加していきましょう↓。

App.js
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: は要らないという点です。なぜならHomeScreenProfileScreenと違って、ヘッダーに表示したいタイトルheaderTitle: と、1階層奥から戻る時の戻るボタンに表示したいタイトルheaderBackTitle: が一緒の文字だからです。navigationOprions: { }は、もしheaderBackTitle: が特別指定されていなかったらheaderTitle: の文字を流用するという暗黙のルールがあります。逆に言えば、HomeScreenProfileScreenはある意味トップ画面なのでヘッダーにはアプリ名("Treco")を表示したいけど、1階層奥の画面から戻る際は別の文字("< Home" や
"< Profile")を表示したいという時は別途headerBackTitle: を指定しなきゃいけません。

5.gif

最後に特別扱いのAddStackAddScreennavigationOprions: { }を追加します。AddScreenは実際に保存したい旅行の詳細情報(国、日付、写真、評価)を入力する画面です。本来StackNavigatorreact-navigation側がデフォルトでヘッダーを用意してくれるのですが(HomeStackProfileStackのように)、AddScreenは上記の使い道の都合上、デフォルトヘッダーでは機能が足りないので自作ヘッダーを後々作成する予定です。なので今はデフォルトヘッダーをなしにする設定をnavigationOprions: { }に追記しましょう↓。

App.js
const AddStack = createStackNavigator({
  add: {
    screen: AddScreen,
    navigationOptions: {
      header: null
    }
  }
});

簡単ですね。header: に「無し」という意味のnullを指定します(falseとはまた別です)。

11.jpg

これでヘッダー周りは完了しました。次はタブにアイコン画像を埋め込むために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フォルダの中に入れときます。

12.png

まずはMainTabの中のhomeStackから始めましょう↓。

App.js
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を忘れずにインポートしましょう。

App.js
import { StyleSheet, Text, View, Image, Platform } from 'react-native';

ではこれで動作確認しましょう!

13.jpg

ちゃんと家の形したアイコンがタブバーに表示されて、かつ押したときは青色、押されてないときは灰色になってますね。もしかしたらiOSシミュレーターが重たいせいでアイコン画像がすぐ出てこないかもしれませんが、実機テストであればすぐ出てきます。

ではこの調子でaddStackprofileStackにもnavigationOprions: { }を付けましょう。ただここでもaddStackは要注意で、他2つとはstyleプロパティが少し異なってます↓。

App.js
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用
});

ではスペルミスがないかチェック後、動作確認してみましょう!

14.png

おおお!大分それっぽくなってきましたね!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をインポートしましょう。

App.js
import { StyleSheet, Text, View, Image, StatusBar, Platform } from 'react-native';

そしてrender()関数の最後のreturn ()の部分で、タグの直上にタグを追記しましょう↓。

App.js
return (
  <View style={styles.container}>
    <StatusBar barStyle="light-content" />
    <NavigatorTab />
  </View>
);

barStyleプロパティを "light-content" にすればステータスバーが白字に、 "dark-content" にすればステータスバーが黒字になります。はい、これでApp.jsおよび画面遷移は完成です!お疲れ様でした。

スマホに情報を保存しよう

ここで次に進む前に、毎回毎回WelcomeScreen.jsが出てくるが鬱陶しいので、「初回起動時にだけ表示してそれ以降は表示しない」という実装をしましょう。ウェルカム画面の最後のページにあるスタートボタンが押されたら「ウェルカム画面表示済み」という情報をスマホに保存し、→次回起動時はまずその情報を読み込んでもし既に「ウェルカム画面表示済み」となっていたらウェルカム画面は表示せずにホーム画面へ飛ばす……という流れです。

まずは、スタートボタンが押されたら「ウェルカム画面表示済み」という情報をスマホに保存する、を実装しましょう。アプリを落とした後もスマホの機体自体に情報を残しておくためには、AsyncStorageという所に保存します。WelcomeScreen.jsreact-nativeからAsyncStorageをインポートします↓。

screens/WelcomeScreen.js
import { StyleSheet, Text, View, ScrollView, Image, Dimensions, AsyncStorage } from 'react-native';

そしてonStartButtonPress()関数に、「AsyncStorageに『ウェルカム画面表示済み』という情報を保存する」という文を書き加えます。'isInitialized' (初期化済みか?という意味)に 'true' をセットします↓。

screens/WelcomeScreen.js
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をつけます↓。

screens/WelcomeScreen.js
// `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コンポーネントのstatenullを初期値として設定しましょう。onStartButtonPress()関数の直上にconstructor()関数を作り、その中でstateを初期化します↓。

screens/WelcomeScreen.js
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);と書いているのはある種のおまじないです。忘れずに付けましょう。

次はstateisInitialized(AsyncStorageの 'isInitialized' とは別物です!)がnullである間、つまりAsyncStorageからの読み取りが終わるまでの間に「只今アプリ読み込み中です」感を出す画面を実装します。その前にnullかどうかを判断する外部ツールを$ npmでインストールします↓。

$ npm install lodash

lodashのインストールが終わったら念のため再度、

$ npm install

をしておきます。ではWelcomeScreen.jsに戻り、先程入れたlodashからアンダーバー_をインポートします(名前が記号って不思議ですね)↓。

screens/WelcomeScreen.js
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からインポートして使いましょう↓。

screens/WelcomeScreen.js
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';

下準備が整ったところで、次は「stateisInitializednullだったらAppLoadingを表示する」を実装しましょう。対象物がnullかどうか判断する文章はlodash_を使用して、

_.isNull(対象物) // ← 対象物が`null`なら`true`を、`null`じゃないなら`false`を返す

と書くことができます。これを応用してrender()関数に「stateisInitializednullだったらいち早くAppLoadingを描画する」と言う風に書きます↓。

screens/WelcomeScreen.js
render() {
  if (_.isNull(this.state.isInitialized)) {
    // もし`state`の`isInitialized`が`null`だったらいち早く`AppLoading`を描画
    return <AppLoading />;
  }

  // もしそうじゃなかったら`ScrollView`を描画
  return (
    <ScrollView
      horizontal
      pagingEnabled
      style={{ flex: 1 }}
    >
      {this.renderSlides()}
    </ScrollView>
  );
}

この状態で動作確認すると、

16.png

ずっとこの画面の状態で止まります。stateisInitializedがずっとnullから変わらないため一向にWelcomeScreenへ進みません笑。これではまずいため、どっかのタイミングでAsyncStorageを読み込んで→その情報を使ってstateisInitializedtruefalseに上書きしなければいけません。

そこで適任な関数が、componentWillMount()です。これはconstructor()関数のrender()関数の中にあるタグやタグやらのコンポーネント達が描画されるに実行される関数です。

constructor()componentWillMount()render()

って順番です。このcomponentWillMount()の中で「AsyncStorageから情報を読み取り→その情報を元にstateisInitializedtruefalseに上書きする」と言う実装をします↓。

screens/WelcomeScreen.js
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の方のisInitializedtrueと上書きしてから 'main' 画面へ飛ばし、

// もし`AsyncStorage`の'isInitialized'から読み込んだ情報が'true'だったら
if (isInitializedString === 'true') {
  // `state`の方の`isInitialized`に`true`と上書き
  this.setState({ isInitialized: true });

  // 'main'画面へ飛ばす
  this.props.navigation.navigate('main');
}

もしそうでなかった場合はstateの方のisInitializedfalseと上書きする

// もし`AsyncStorage`の'isInitialized'から読み込んだ情報が'true'じゃなかったら
} else {
  // `state`の方の`isInitialized`に`false`と上書き
  this.setState({ isInitialized: false });
}

という感じです。これでロジックは完成なのですが、ただAsyncStorageから保存した情報を読み取るのも非同期処理なので、「処理が終わるまで待ってあげるよ」宣言を忘れずにしなければいけません。なのでasyncを関数(今回はcomponentWillMount())の先頭に、awaitを実際に待機する文(今回はAsyncStorage.getItem())の先頭に付けます↓。

screens/WelcomeScreen.js
// ↓`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 });
  }
}

17.png

awaitを介して変数に代入するために、isInitializedString変数の接頭辞はconstではなくletにしたのです。

これでWelcomeScreen.jsは完成したので、動作確認してみましょう。

6.gif

今度は、command⌘+rで更新したらちゃんとウェルカム画面が表示されるようになりましたね。

constructor()stateの方のisInitializednullに初期化される

componentWillMount()AsyncStorageの方の 'isInitialized' が 'true' かどうか読み込み始める

render()が実行されるが、stateの方のisInitializedはまだnullなので、一旦が描画される

componentWillMount()でのAsyncStorageの読み込みが完了し、stateの方のisInitializedtrueに上書きされる

stateの値に変更がある度に再度render()が実行されるというルールがあるため、今度はが描画される

という流れです。いくらAsyncStorageの読み書きには少し時間を要するとは言え、最近のスマホは高スペックなので実際②〜④は目に見えないほど一瞬で終わります。この流れは、試しにrender()関数でではなく真っピンクのを描画するように変更してみるともっとわかりやすくなります。

screens/WelcomeScreen.js
return <AppLoading />;



//return <AppLoading />;
return <View style={{ flex: 1, backgroundColor: 'pink' }} />;

7.gif

一瞬だけ真っピンクの画面が現れますね。これが上記の②〜④の間、つまりstateの方のisInitializednullである間のことですね。componentWillMount()async/awaitの動きが掴めたら、ちゃんとrender()関数を元に戻しておきましょう笑。

screens/WelcomeScreen.js
//return <AppLoading />;
return <View style={{ flex: 1, backgroundColor: 'pink' }} />;



return <AppLoading />;

8.gif

これでWelcomeScreen.jsは完成です!上図の通りウェルカム画面の最終ページのスタートボタンを押すと、もうそれ以降いくらcommand⌘+rで更新してもウェルカム画面は出てこなくなります(最終的には練習のためにウェルカム画面を復活させるボタンも作りますのでご心配なく笑)。では次章から本題のホーム画面へ入っていきましょう。次章ではなんとあのReact Native第一の難関、Reduxが遂に登場します……でも大丈夫です、図解でわかりやすく解説していきますのでご安心ください ;->

ホーム画面の見た目を作ろう

Reduxとかいう魔のフレームワークの前に、まずはホーム画面HomeScreen.jsの見た目から作りましょう。メルカリなどのアプリでもよく見かける、評価一覧を「すべて / 良い / 普通 / 悪い」でグループ分けできるボタンを作成します。

18.png

この機能は、既にインストール済みのreact-native-elementsの中にButtonGroupという名前で含まれています。早速HomeScreen.jsにて(WelcomScreen.jsじゃないですよ!)インポートしましょう↓。

screens/HomeScreen.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { ButtonGroup } from 'react-native-elements';
          // ↑コレ


class HomeScreen extends React.Component {
  // ゴニョゴニョ…

インポートしたら、render()関数内を書き換えてを表示させましょう。

screens/HomeScreen.js
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」の後ろについている括弧付き数字は、各評価の数を表していますが、今はとりあえずゼロにしておきます。

19.png

この状態で動作確認してみましょう。

9.gif

ちゃんと表示されていますね!しかしこのままでは味気ないので、ギミックを付け加えていきましょう。現段階では、最初は All も Great も Good も Poor もどれも選択されていませんが、普通は大体最初から All が選択されてますよね。このギミックをstateと、タグのselectedIndexプロパティを使って実装します↓。

screens/HomeScreen.js
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()関数でstateselectedIndex0に初期化し、その値this.state.selectedIndexをの方のselectedIndexプロパティにぶち込みます。そうすると、

20.png

最初から All ボタンが選択された状態でアプリが起動する様になります。タグでは左のボタンから順に、0番から番号が付いています(配列と同じく先頭番号は0番です)ので、stateselectedIndex

0だとAll|1だとGreat|2だとGood|3だとPoor

に割り当てられます。

しかし現状では、 Great / Good / Poor のどのボタンを押しても一瞬光るだけで All から切り替わってくれません(試しに各ボタンを押してみてください)。なので次は、 Great / Good / Poor の各ボタンが押されたら、All からそのボタンに切り替わるというギミックを実装します。以前使用したタグと同様に、タグでもonPressプロパティを使用します↓。

screens/HomeScreen.js
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)」が入力として入ってくるので、すかさずstateselectedIndexをその入力そのままの値で更新します。

③ すると連動してタグの方のselectedIndexプロパティも更新されるので、アプリ画面上のボタンが切り替わります。

21.png

という流れです。では動作確認してみましょう。

10.gif

ちゃんとボタンを押すたびに切り替えられてますね。では次は仮の評価データ達を作り、それらをリスト表示する実装をしましょう。

仮のデータ達は、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定義文の間に書きます↓。

screens/HomeScreen.js
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()というリスト表示専用の関数を新たに用意しましょう↓。

screens/HomeScreen.js
renderReviews() { // ← 追記部分
  return (

  );
}


onButtonGroupPress = (selectedIndex) => {
  // ゴニョゴニョ…
}


render() {
  // ゴニョゴニョ…

  return (
    <View style={{ flex: 1 }}>
      <ButtonGroup
        buttons={buttonList}
        selectedIndex={this.state.selectedIndex}
        onPress={this.onButtonGroupPress}
      />

      {this.renderReviews()} // ← 追記部分
    </View>
  );
}

これからrenderReviews()関数の中身を書いていくのですがその前に、

ScrollViewreact-nativeから
ListItemreact-native-elementsから

インポートしてください。

screens/HomeScreen.js
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を使います↓。

screens/HomeScreen.js
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という数字が一体何のことを意味しているのか覚えていないかもしれません。もしくは他人の同僚が見た時にパッと見で数字の意味が伝わりづらいです。ひょっとしたら周辺のコードをじっくり読み返せばわかるかもしれませんが、それはそれでだるいので、こういった意味を持つ数字にはちゃんと名前を付けてあげましょう ↓。

screens/HomeScreen.js
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の初期化に使っていた0ALL_INDEXに置き換えました。これで、「ん〜と0, 1, 2, 3ってどういう意味だったっけ…?」と迷うことが無くなります。

話を戻しましょう。次は、「仮データの中から特定の評価のデータだけ抜き取る」という作業を実装します。より具体的には、「仮データを1個ずつ見て行って特定の評価(GREAT, GOOD, POORのどれか)と一致してる奴だけを新たな配列に順次追加していく」ということをします。今回のように同じ処理を繰り返し行うといった時は for 文という構文を使うと便利です↓。

for (どこから繰り返すか; いつまで繰り返すか; 何ステップずつ繰り返すか) {

  // 繰り返したい処理

}

より具体的には、

for (let i = 0; i < 10; i++) {

  // 繰り返したい処理

}

と書くと、i0から1ずつ増えていって9になるまでの計10回分繰り返してくれます。iが0から10までじゃないことに気をつけて下さい(計11回分になってしまうため)。ちなみにi++i = i + 1の略で、「今のiの値に1を足す」という意味です。したがって、

for (let i = 1; i <= 10; i++) {

  // 繰り返したい処理

}

こう書いても確かにi1から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()関数が使えます。新要素を配列の中に後ろから押し込む感じなので、プッシュ関数という名前です↓。

screens/HomeScreen.js
// 新たな空の配列を用意
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.selectedIndexALL_INDEXの時)は当然データを全部表示するので、rankedReviewsallReviewsTmpを丸ごとコピーします↓。

screens/HomeScreen.js
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配列の個数分だけを描画する」です↓。

screens/HomeScreen.js
renderReviews() {
  // ゴニョゴニョ…

  return (
    <ScrollView>
      {rankedReviews.map((review, index) => {
          return (
            <ListItem />
          );
        })
      }
    </ScrollView>
  );
}
screens/HomeScreen.jsrenderReviews() {
  // ゴニョゴニョ…

  return (
    <ScrollView>
      {rankedReviews.map((review, index) => {
          return (
            <ListItem />
          );
        })
      }
    </ScrollView>
  );
}

今回は.map()関数(に渡されてるアロー関数)にreviewindexを渡しています。

  1. rankedReviews配列の各要素の一つ…review
  2. 現要素の番号...index

22.jpg

まずはrankによって描画するアイコン色を変えるようにしましょう。ここでもまた switch 文が出てきます↓。

screens/HomeScreen.js
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'をそのまま書くのではなく、変数に置き換えましょう↓。

HomeScreen.js
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…サブタイトル。文字色は灰色

screens/HomeScreen.js
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>
);

特殊なのはleftIconsubtitleですね。leftIconはJavaScriptオブジェクトで、

name...アイコンの名前。'sentiment-very-satisfied' など
color...アイコンの色。'red' など

の2項目を指定します。これら2項目は、HomeScreen.jsの上の方にconst GREATとかconst GREAT_COLORとかって大文字の名前で定義し直したやつですね。

subtitleは変数の数値をテキストに変換するバッククォート機能(正式名はテンプレート文字列)を使います。テキストは普通シングルクォートで囲むのですが、バッククォートで囲んだ中で${変数名}と入れると、そこの部分のテキストだけ変数の数値が表示されるようになります。シングルクォートの中で '${変数名}' とやっても何も起きません、そのまま文字通り表示されます笑。

いやーここまで長かったですね!やっと動作確認できるとこまで来ました!

11.gif

ちゃんと Great / Good / Poor 別に分かれていますね。因みにタグのsubtitleプロパティでのテンプレート文字列について、バッククォートとシングルクォートの違いを比較した画像がこちらです↓。

23.jpg

バッククォートで囲んだ方は変数の値がちゃんと反映されているのに対し、シングルクォートで囲んだ方はそのまま文字通り表示されちゃってます。

では次にこのタグ(=レビューデータ)を押したらDetailScreen.jsに飛ぶように追加しましょう。ここで使うのはonPressプロパティです↓。

screens/HomeScreen.js
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' に画面遷移するように書きます↓。

screens/HomeScreen.js
constructor(props) {
  // ゴニョゴニョ…
}


// `onPress`からの引数は`selectedReview`という名で受け止める(一旦放置。後で使用)
onListItemPress = (selectedReview) => {
  // 'detail'に飛ぶ
  this.props.navigation.navigate('detail');
}


renderReviews() {
  // ゴニョゴニョ…
}

では動作確認しましょう。各レビューデータをクリックしてみて下さい。

12.gif

だいぶ形になって来ましたね。最後に Great / Good / Poor の末尾にある数字の (0) の所を、データの個数を表すように変えましょう。これもこれまでにやって来たのと同じように、switch文でGREATかGOODかPOORかを判定するのをfor文でひたすら繰り返すだけです。render()関数の中にfor文とswitch文を追加し、buttonListも先程出てきたバッククォートによるテンプレート文字列に変更します↓。

screens/HomeScreen.js
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です。それでは動作確認してみましょう。

24.png

ちゃんとデータの個数が反映されていますね!では次章からついにあの魔の Redux が始まります…。

Reduxとは

初回起動かどうか」や「今All / Great / Good / Poorのどのボタンが押されているか」などといったアプリの”状態”を表すには、今までthis.stateという物を使って表現してきました。しかしthis.stateは同じファイルの中でしか有効ではないため、例えばHomeScreen.jsthis.stateHomeScreen.js内からしかアクセスできず、他のDetailScreen.jsなどからは見れません。したがってファイル間( ≒コンポーネント間)をまたがってアプリ全体で情報を共有したい時、特に画面遷移を伴う時はちょっと違った手を使わねばなりません。そこで登場するのがこのReduxというシステムです。イメージとしては、データ保存場所が別で用意されており(正式名はStore)、どのページからもそこに保存されている共通データにアクセスできるという感じです。

25.jpg

ただ、この共通データ保存場所であるStoreにデータを保存するまでがまわりくどいのなんの……。とりあえずまずは、「Storeの共通データへどのページからもアクセスできるようにする」を実装しましょう。React NativeでReduxを使うのに必要な3つのものを$ npmコマンドでインストールし、

この続きはnote.muにて

(残り55,057文字 / 画像33枚)

Screen Shot 2018-06-09 at 20.00.40.png

30
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?