見出し画像

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


本記事は2018年5月に執筆されました.当時の React Native の環境から変わっている部分もありますので,2020年5月より記事の値段を大幅値下げすると同時に,記事更新や質問等のサポートの対象外とさせて頂きます.
m(_ _ )m

1/3 【ウェルカム画面編】:799円 → 199円
2/3 【ホームタブ編】:899円 → 299円
3/3 【プラスボタン編】:999円 → 399円

1. ウェルカム画面編 
2. ホームタブ編 ← 今ココ
3. プラスボタン編

画像1


note.muの中の方からもコメント頂けました!ありがとうございます!

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

画像2

↑前回記事までの目標

画像3

↑この記事での目標

この記事で得れる物

・上図のスマホアプリが作れるようになる
・React Nativeの流れを図で直感的にわかる
・なぜこのようにプログラミングをしたのかまでわかる

この記事の対象者

・アプリ開発経験そんなない方
・Web系開発経験そんなない方
・でもまあほんのちょっとプログラミングかじった事ある方

大丈夫です、僕も当時こんな感じでした。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を学ぶ際に実際に引っかかった点を主に、「もっとこうやって教えてくれたら良かったのに」という視点から超初心者(かつての自分)向けにスクショ豊富で書いてます。わかりやすさを優先しているためざっくりとした説明が多く、本当の意味での正確性には欠けているかもしれません。ただ読み進めてコード書き写してくだけでそれなりのアプリが作れるようになる、そんな記事にしたいと思います。実際にあなたのスマホ上で作ったアプリを動かしますので、スマホからではなくPCからの閲覧推奨です。

作るアプリは"旅行記アプリ"です。過去に自分が行った国と日付を入力し、その時の思い出の写真とその旅行の3段階評価を付けて保存していく、というアプリです(Trip + record = Treco)。まあ使い道があるかどうかはいいとして(笑)、このアプリにはよくある要素の

・真ん中が `+` プラスボタンになってるタブ構成 & 画面遷移
・日付等を選ぶプルダウンメニュー
・地図や画像の配置

などが多く積み込まれています。あとはこれらの基礎要素を応用して組み替えたりしていけば、大体頭の中で想像しているアプリを実装できるようになりますし、何よりこのチュートリアルシリーズを読み終わってる頃には、自力で検索して必要な情報だけを探せれるようになっています。

前回記事では、初回起動時に表示されるウェルカム画面(左右にスライドできて各ページにアプリの説明が書いてあるやつ)を作りましたね。本記事では以上のような旅行記アプリの、旅の記録一覧がズラーっと並ぶホーム画面を実装します。2回目以降にアプリを立ち上げた時に最初に表示される画面ですね。この回からグッと学ぶ事増えますが大丈夫です、スクショ&図解付きでわかりやすく説明していきます。

1. ウェルカム画面編 
2. ホームタブ編 ← 今ココ
3. プラスボタン編

画面遷移を付けよう

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

画像4

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

幸運な事にReact Nativeには画面遷移の実装を超楽にしてくれる`react-navigation`と言うものがあるので、前回記事と同じようにターミナル
`$ npm`コマンドを使って外部からインストールしましょう。

画像5

$ npm install react-navigation@3.0.0
※ 2020年2月8日の時点で react-navigation の最新版は ver. 5 ですが,書き方が少し複雑になっているので,初学者向けの本記事ではよりシンプルに書ける ver. 3 で説明します.

ここで追加でいくつかインストールします.

$ expo install react-native-gesture-handler react-native-reanimated react-native-screens

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

$ npm install

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

import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { createAppContainer, 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() {
    // ゴニョゴニョ…
  }
}

App.js

先ほどインポートした`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`の中に、`createAppContainer()`関数と`createBottomTabNavigator()`関数を使ってタブ遷移の詳細を追記して行きます。

export default class App extends React.Component {
  render() {
    const NavigatorTab = createAppContainer(
      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',
  },
});

App.js

注意点が2つあります。1つ目は冒頭のイメージ図でも示した通り、`BottomTabNavigator`の中に更にもう1つ`BottomTabNavigator`が入れ子になっている事です(プログラミングではこういう事よくあります)。

2つ目はソースコード最下部のstylesオブジェクトで`alignItems: 'center' `を消すかコメントアウトすることです。`alignItems: 'center' `があると何故か`react-navigation`が上手く動かず、画面真っ白になってしまいます。

※ JavaScriptのコメントアウトとは先頭にスラッシュ2つ`//`を付けることで、コンピューター側からするとその文はなかったことになります。でも消すにはもったいないとか、メモ書きを残すとか、そういった人間側の都合でコメントアウトは頻繁に使われます。

次に先ほどまだ作ってもないのにインポートした、`HomeScreen` / `AddScreen` / `ProfileScreen`のメイン3画面を新規作成しましょう。`WelcomeScreen.js`と同じ`screens`フォルダの下に新たに`HomeScreen.js`, `AddScreen.js`, `ProfileScreen.js`を作成し、

画像6

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

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/HomeScreen.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/AddScreen.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;

screens/ProfileScreen.js

これでタブ遷移の準備は整ったので、動作確認してみましょう。 前回と同じくExpo Developer Toolsの`Run on iOS simulator`をクリックします。

画像7

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

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

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

App.js

この時点では書き方をスッキリさせたたけですので、アプリの挙動自体は何ら変わっていません。次に(やっと)親`BottomTabNavigator`のタブを非表示にします。そのためには、`NavigatorTab`の方の`createBottomTabNavigator()`関数を`createSwitchNavigator()`関数に置き換えます。`createSwitchNavigator`を`rect-navigation`からインポートして、

import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import {
  createAppContainer,
  createBottomTabNavigator,
  createSwitchNavigator // ←追記部分
} 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() {
    const MainTab = createBottomTabNavigator({
     homeStack: { screen: HomeScreen },
     addStack: { screen: AddScreen },
     profileStack: { screen: ProfileScreen }
   });

   const NavigatorTab = createAppContainer(
     createSwitchNavigator({ // ←変更部分
       welcome: { screen: WelcomeScreen },
       main: { screen: MainTab }
     })
   );

    :
    :

App.js

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

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');
  }

  // ゴニョゴニョ…

}

変更後「メインタブに飛ばす」 screens/WelcomeScreen.js

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

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

画像8

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

画像9

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

import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import {
 createAppContainer,
 createBottomTabNavigator,
 createSwitchNavigator,
 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() {
    // ゴニョゴニョ…
  }
}

App.js

また、新たに`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 } // ←変更部分
});

App.js

画像10

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

1. `HomeScreen` ⇆ `DetailScreen`を行き来するボタン
2. `AddScreen` → `HomeScreen`に戻るボタン(一方通行)
3. `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/HomeScreen.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/DetailScreen.js

`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/AddScreen.js

`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/ProfileScreen.js

`screens/Setting1Screen.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/Setting1Screen.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/Setting2Screen.js

`screens`フォルダの中にある`WelcomeScreen.js`以外の6ファイルは全部いじりました。整理すると、

・編集:`HomeScreen.js`, `AddScreen.js`, `ProfileScreen.js`
・新規作成:`DetailScreen.js`, `Setting1Screen.js`, `Setting2Screen.js`

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

画像11

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

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

画像12

・`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)
  };
};

App.js

画像13

こうすることで、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 = createAppContainer(
      createSwitchNavigator({
        welcome: { screen: WelcomeScreen },
        main: { screen: MainTab }
      })
    );


    // `NavigatorTab`を描画
    return (
      <View style={styles.container}>
        <NavigatorTab />
      </View>
    );
  }
}

App.js

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

画像14

ちゃんと希望の階層以降ではタブが隠れてますね!こうする事によって例えば、`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',
};

App.js

・`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 }
});

App.js

違和感ある変な書き方ですが、ドット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 }
});

App.js

後は足りない項目(他のスクリーン達と共通ではない部分)を付け足します。

・`headerTitle: `…ヘッダーのタイトル
・`headerBackTitle: `…一個奥の画面から戻る際の、左上の戻るボタンの文字

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

画像15

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

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

App.js

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

画像16

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',
    }
  }
});

App.js

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

画像17

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

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

App.js

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

画像18

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

画像19

まずは`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用
});

App.js

`tabBarIcon: `は、アロー関数を用いて<Image/>タグを適応し、`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`を<Image/>タグのstyleプロパティの中の`tintColor: `に与えてあげれば、今いるタブだけ青く光らせることができます。<Image/>タグは、

・`style: `…Imageタグの装飾全般。更ににオブジェクトが入る
  ・`height: `…画像の高さ
  ・`width: `…画像の幅
  ・`tintColor: `…画像の塗り潰し色
・`source: `…画像の保存場所。要`require()`関数

という内訳になっています。またこれはJavaScriptオブジェクトのちょっとした省略ルールですが、もし項目名と内容名が同じ場合、

{ tintColor: tintColor }

  ↓

{ tintColor }

と略すこともできます。お好みでどうぞ。最後に`react-native`から`Image`を忘れずにインポートしましょう。

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

App.js

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

画像20

ちゃんと家の形したアイコンがタブバーに表示されて、かつ押したときは青色、押されてないときは灰色になってますね。もしかしたら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用
});

App.js

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

画像21

おおお!大分それっぽくなってきましたね!`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';

App.js

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

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

App.js

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';

screens/WelcomeScreen.js

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

onStartButtonPress = () => {
  // `AsyncStorage`に『ウェルカム画面表示済み』という情報を保存する
  AsyncStorage.setItem('isInitialized', 'true');

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

screens/WelcomeScreen.js

'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');
}

screens/WelcomeScreen.js

そしたら次は、起動時に毎回`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');
  }

  // ゴニョゴニョ…

}

screens/WelcomeScreen.js

`constructor()`関数は一番最初に実行される関数のことで、この関数の中で`state`を初期化します。`props`を入力として入れてかつ最初に`super(props);`と書いているのはある種のおまじないです。忘れずに付けましょう。

しかしこれでは`state`の`isInitialized`がずっと`null`から変わらないため、どっかのタイミングで`AsyncStorage`を読み込んで→その情報を使って`state`の`isInitialized`を`true`か`false`に上書きしなければいけません。

そこで適任な関数が、`componentDidMount()`です。`render()`関数の中にある<View>タグや<Text>タグやらのコンポーネント達が描画された後に自動的に実行される関数です。

`render()` → `componentDidMount()`

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

constructor(props) {
  // ゴニョゴニョ…
}


componentDidMount() {
  // `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 () => {
  // ゴニョゴニョ…
}

screens/WelcomeScreen.js

内容を解説をすると、まず`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`を関数(今回は`componentDidMount()`)の先頭に、`await`を実際に待機する文(今回は`AsyncStorage.getItem()`)の先頭に付けます↓。

// ↓`await`を使う関数は文頭に`async`と書く必要がある
async componentDidMount() {
  // `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 });
  }
}

画像22

screens/WelcomeScreen.js

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

またAsycnStorageから 'isInitialized' を読み込んでる間、つまり `this.state.isInitialized` がtrueでもfalseでもなくまだnullのままの間は、「アプリ読み込み中です」を示すグルグルマークを表示させます。'react-native'から`ActivityIndicator`をインポートし、`render()`関数内に追記します。

import React from 'react';
import {
 StyleSheet, Text, View, ScrollView, Image, ActivityIndicator, // ←追記部分
 Dimensions, AsyncStorage
} from 'react-native';
import { Button } from 'react-native-elements';

// ゴニョゴニョ...

class WelcomeScreen extends React.Component {

  // ゴニョゴニョ...

  render() {
    if (this.state.isInitialized === null) { // ←追記部分
      return <ActivityIndicator size="large" />;
    }

    return (
      <ScrollView
        horizontal
        pagingEnabled
        style={{ flex: 1 }}
      >
        {this.renderSlides()}
      </ScrollView>
    );
  }
}

screens/WelcomeScreen.js

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

画像23

①`constructor()`で`state`の方の`isInitialized`が`null`に初期化される

②`state`の方の`isInitialized`が`null`なので一旦 <ActivityIndicator />が表示される

③`componentDidMount()`で`AsyncStorage`の方の 'isInitialized' が 'true' かどうか読み込み始める

④`componentDidMount()`での`AsyncStorage`の読み込みが完了し、`state`の方の`isInitialized`が`true`に上書きされる

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

という流れです。いくら`AsyncStorage`の読み書きには少し時間を要するとは言え、最近のスマホは高スペックなので実際②〜④は目に見えないほど一瞬で終わります。

画像24

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

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

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

画像25

この機能は、既にインストール済みの`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 {
  // ゴニョゴニョ…

screens/HomeScreen.js

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

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>
    );
  }
}

screens/HomeScreen.js

<ButtonGroup />は、buttonsプロパティに表示したいボタンのリスト(今回は評価ランクの「すべて / 良い / 普通 / 悪い」)を配列で渡すと、横一列にボタン群を綺麗に表示してくれます。なので、buttonsプロパティに配列の`buttonList`を渡しており、`buttonList`の配列の中身は「すべて / 良い / 普通 / 悪い」の英語訳に当たる「All / Great / Good / Poor」を入れています。ここで注意は、buttonsプロパティはHTML風(正確にはJSXと呼ぶ)なのに対して`buttonList`はJavaScriptなので、ちゃんと波括弧`{ }`で囲んで「今からJavaScript書くよ〜」と示すのを忘れない事です。波括弧`{ }`を忘れると真っ赤なエラーに見舞われます汗。また「Great / Good / Poor」の後ろについている括弧付き数字は、各評価の数を表していますが、今はとりあえずゼロにしておきます。

画像26

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

画像27

ちゃんと表示されていますね!しかしこのままでは味気ないので、ギミックを付け加えていきましょう。現段階では、最初は All も Great も Good も Poor もどれも選択されていませんが、普通は大体最初から All が選択されてますよね。このギミックを`state`と、<ButtonGroup />タグの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>
    );
  }
}

screens/HomeScreen.js

まずは`constructor()`関数で`state`の`selectedIndex`を`0`に初期化し、その値`this.state.selectedIndex`を<ButtonGroup />の方のselectedIndexプロパティにぶち込みます。そうすると、

画像28

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

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

に割り当てられます。

しかし現状では、 Great / Good / Poor のどのボタンを押しても一瞬光るだけで All から切り替わってくれません(試しに各ボタンを押してみてください)。なので次は、 Great / Good / Poor の各ボタンが押されたら、All からそのボタンに切り替わるというギミックを実装します。以前使用した<Button />タグと同様に、<ButtonGroup />タグでも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>
  );
}

screens/HomeScreen.js

① 各ボタンが押されるたびにonPressプロパティに設定されている関数`this.onButtonGroupPress`(に紐付けられているアロー関数)が発動します。

② その際にonPressプロパティから「今押されたボタンの番号(0~3)」が入力として入ってくるので、すかさず`state`の`selectedIndex`をその入力そのままの値で更新します。

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

画像29

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

画像30

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

仮のデータ達は、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 {
  // ゴニョゴニョ…

screens/HomeScreen.js

各データに必要な項目の内訳は、

・`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>
  );
}

screens/HomeScreen.js

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

・`ScrollView`を`react-native`から
・`ListItem`を`react-native-elements`から

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

import { StyleSheet, Text, View, ScrollView } from 'react-native';
                                  // ↑コレ
import { ButtonGroup, ListItem } from 'react-native-elements';
                      // ↑コレ

screens/HomeScreen.js

前回のウェルカム画面と同じ様に、<ScrollView>の中で配列の個数分だけ今度は<View>じゃなくて<ListItem>を描画する、という作戦です。

では評価データ達を、評価ランクごとに仕分けするロジックを作っていきましょう。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 (

  );
}

screens/HomeScreen.js

これで`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 (

    );
  }

  // ゴニョゴニョ…
}

screens/HomeScreen.js

ついでに`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]);
  }
}

screens/HomeScreen.js

これで、新たに用意した配列`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]);
    }
  }
} // ←追記部分

screens/HomeScreen.js

これで評価別に分けたデータが完成したので、次は実際に描画するところを実装します。配列の個数分だけ同じ描画を繰り返すのに便利なのは前回と同じく、`.map()`関数です。先程のfor文との違いは、繰り返し"処理"なのか、繰り返し"描画"なのかで(私は)使い分けています。何かコンポーネントを描画するということは必ず`return ()`文があるので、「returnするなら`.map()`関数、returnしないならfor文」ということになります。

今回`.map()`関数で繰り返すことは、「<ScrollView>の中で`rankedReviews`配列の個数分だけ<ListItem>を描画する」です↓。

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

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

screens/HomeScreen.js

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

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

画像31

まずは`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>
  );
}

screens/HomeScreen.js

またここでも今後のことを考えて'red', 'orange', 'blue'をそのまま書くのではなく、変数に置き換えましょう↓。

const ALL_INDEX = 0; 

const GREAT = 'sentiment-very-satisfied';
const GREAT_INDEX = 1;
const GREAT_COLOR = 'red'; // ← 追記部分

const GOOD = 'sentiment-satisfied';
const GOOD_INDEX = 2;
const GOOD_COLOR = 'orange'; // ← 追記部分

const POOR = 'sentiment-dissatisfied';
const POOR_INDEX = 3;
const POOR_COLOR = 'blue'; // ← 追記部分

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>
    );
  }

  // ゴニョゴニョ…
}

HomeScreen.js

これで評価別のアイコン色の指定は完了しました。次は<ListItem />の中身を書いていきます。<ListItem />タグには、以下のプロパティを与えます。

・`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>
);

screens/HomeScreen.js

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

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

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

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

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

画像32

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

画像33

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

では次にこの<ListItem />タグ(=レビューデータ)を押したら`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)} // ←追記部分
  />
);

screens/HomeScreen.js

後々使うので、`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() {
  // ゴニョゴニョ…
}

screens/HomeScreen.js

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

画像34

だいぶ形になって来ましたね。最後に 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(
    // ゴニョゴニョ…
  );
}

screens/HomeScreen.js 

Allの時は`allReviewsTmp`配列にあるデータ数をそのまま出せば良いので、`allReviewsTmp.length`でOKです。それでは動作確認してみましょう。

画像35

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

Reduxとは

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

画像36

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

この続きをみるには

この続き:35,122文字/画像33枚

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

川西発之 / 陳発暉

899円

この記事が気に入ったら、サポートをしてみませんか?気軽にクリエイターを支援できます。

note.user.nickname || note.user.urlname

チュートリアル完成しましたらTwitterでのご報告お待ちしております!笑

アプリ完成しましたらTwitterでご報告お待ちしております!笑
36
高専情報科(C/C++)→大学機械科→HWエンジニアインターン at ドローンベンチャー(Python)→SWエンジニアインターン in NY(PHP)→ニートしながらアプリ開発(React Native)→大学院で自動運転の研究(C++)。日本生まれの純血中国人🇯🇵🇨🇳

コメント9件

へのへのもへじ様

返信が遅れしまいすみません,ご指摘ありがとうございます!
react-navigation について ver. 3 に対応致しました.
LoA様

返信が遅れしまいすみません,コメントありがとうございます!

恐らくLoA様のコードは ver. 4 の react-native かと思われます.
react-navigation の ver. についてですが,ver. 4 および ver. 5 は npm install する物が多くなるので,初学者を対象にした本記事ではよりシンプルな方が良いと思い ver. 3 で説明することにしました.

混乱を招く様なことになってしまい申し訳ありません,ご指摘ありがとうございました.
とても勉強になります。
ところで新しいexpoのバージョンだとMapViewが別になってるかもしれません。
expo install react-native-maps
import MapView from 'react-native-maps';

react-native-geocoding の書き方も変わっているようでした。
Geocoder.init
Geocoder.from
nosotros様

ご指摘ありがとうございます!
nosotros様のコメントを基にこちらでも動作確認を行い,記事の内容を修正しました.
コメントを投稿するには、 ログイン または 会員登録 をする必要があります。