SwitchNavigatorを使ってみる


Login, Logout画面の作成にはSwitchNavigatorを使えばいいと言われていますが、使い方がよくわからなかったので調べてみました。
ついでに、Stack, Tab, Drawer全てのNavigationを組み合わせてみます。

なお、数ある記事の中でも[この記事](https://medium.com/the-react-native-log/building-an-authentication-flow-with-react-navigation-fb5de2203b5c)が大変参考になりました。


## はじめに（お断り）

この記事では認証機能自体は実装しません。あくまで認証したことを前提に「認証済み」状態をflag情報としてAsyncStorageに記録し、それを元にSwitchNavigatorを機能させることがゴールです。

## ファイル構成と基本フロー

ファイルは

1. Home.js
2. Profile.js
3. SignIn.js
4. SignUp.js

の４つだけです。

このうち、HomeとProfileはサインイン後のTOP画面を構成するTab Navigator画面を、SignUpとSignInはサイン前のTOP画面を構成するStack Navigator画面を構成します。また、サイン後はどこでサインアウトができるように、Drawerを仕込んでいます。

ログイン済みかどうかはローカルストレージ（AsyncStore）にflag情報を保持することで管理します。

>実運用アプリではRedux storeとかになるでしょうか。

イメージにすると下記のような感じ。

![スクリーンショット 2018-11-05 11.38.24.png](https://qiita-image-store.s3.amazonaws.com/0/55188/9f406ec0-1bb0-86e7-1978-95e7dcb60538.png)



## 基本ファイルの準備

まずは基本ファイルを準備して、単純に表示させてみます。

### ファイルの準備

```bash
expo init switch
cd switch
mkdir screens
cd screens
touch Home.js Profile.js SignIn.js SignUp.js
```

### Home.js

Homeと表示するだけの単純なファイルです。

```js
import React from 'react';
import { View, Text, Button } from 'react-native';

class Home extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text>Home</Text>
            </View>
        );
    }
}

export default Home;
```

### Profile.js

Profileと表示するだけの単純なファイルです。あとはHomeと同じ。

```js
import React from 'react';
import { View, Text, Button } from 'react-native';

class Profile extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text>Profile</Text>
            </View>
        );
    }
}

export default Profile;
```

### SignIn.js

SignInと表示するだけの単純なファイルです。あとはHomeと同じ。

```js
import React from 'react';
import { View, Text, Button } from 'react-native';

class SignIn extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text>SignIn</Text>
            </View>
        );
    }
}

export default SignIn;
```

### SignUp.js

SignUpと表示するだけの単純なファイルです。あとはHomeと同じ。

```js
import React from 'react';
import { View, Text, Button } from 'react-native';

class SignUp extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text>SignUp</Text>
            </View>
        );
    }
}

export default SignUp;
```

### App.js

では、代表してHome.jsを表示してみます。

```js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

import Home from './screens/Home';
import Profile from './screens/Profile';
import SignIn from './screens/SignIn';
import SignUp from './screens/SignUp';

export default class App extends React.Component {
  render() {
    return (
      <Home />
    );
  }
}
```

## 基本フローの実装

次にStackNavigatorやTabNavigator、リンク等を設置してアプリのフローを実装します。まだ認証機能は実装しません。

### 必要なモジュールのインストール

react-navigationを利用するのでインストールします。

```bash
npm install --save react-navigation
npm install --save react-native-gesture-handler
npm install --save react-native-reanimated
npm install
```

### 追記（バージョン修正）

2019年8月時点の環境では(expo34)、バージョンダウンしろ！的なアラートが出ます。
package.jsonのreact-native-gesture-handlerとreact-native-reanimatedの箇所をバージョンダウンします。

```js:package.json
    "react-native-gesture-handler": "~1.3.0",
    "react-native-reanimated": "~1.1.0",
```

バージョンダウンしたら、node_modulesフォルダとpackage-lock.jsonを削除した上で、

```bash
npm install
```

を実行します。なお、expo利用の場合は、さらに

```bash
expo r -c
```

としてキャッシュをクリアする必要があるようです。


### App.js

App.jsを編集し、StackNavigator, TabNavigator, DrawerNavigatorの設定をしていきます。

#### 追記（2018年11月23日）

react-navigation v3ではcreateAppContainer()を利用する必要があります。

```js
import React from 'react';
import { StyleSheet, Text, View, Button } from 'react-native';
import {
    createStackNavigator,
    createBottomTabNavigator,
    createDrawerNavigator,
    createSwitchNavigator,
    DrawerItems,
    createAppContainer,
    SafeAreaView
} from 'react-navigation';

//パーツ読み込み
import Home from './screens/Home';
import Profile from './screens/Profile';
import SignIn from './screens/SignIn';
import SignUp from './screens/SignUp';

//Tab画面（headerの高さ調整のためにStackNavigatorでラップしておく）
const HomeTab = createBottomTabNavigator(
    {
        Home: { screen: createStackNavigator({ Home: { screen: Home } }) },
        Profile: { screen: createStackNavigator({ Profile: { screen: Profile } }) }
    }
);

//DrawerをかましてHomeTabを表示
const SignedIn = createDrawerNavigator(
    {
        Home: { screen: HomeTab }
    },
    {
        contentComponent: (props) => (
            <View style={{ flex: 1 }}>
                <SafeAreaView forceInset={{ top: 'always', horizontal: 'never' }}>
                    <DrawerItems {...props} />
                    <Button
                        title="Logout"
                        onPress={() => props.navigation.navigate('SignedOut')}
                    />
                </SafeAreaView>
            </View>
        )
    }
);

//SignOut時の標準画面
const SignedOut = createStackNavigator(
    {
        SignUp: { screen: SignUp },
        SignIn: { screen: SignIn }
    }
);

//Switchを定義
const createRootNavigator = (signedIn = false) => {
    return createSwitchNavigator(
        {
            SignedIn: { screen: SignedIn },
            SignedOut: { screen: SignedOut }
        },
        {
            initialRouteName: signedIn ? 'SignedIn' : 'SignedOut'
        }
    );
}

//AppContainerでラップ（RN v3より）
const Layout = createAppContainer(createRootNavigator(true));

export default class App extends React.Component {
    render() {
        return (
            <Layout />
        );
    }
}
```

### Home.js

Home.jsは特に変更しません。

### Profile.js

サインアウトボタン（リンク）を準備し、SignedOutにリンクするようにします。

```js
import React from 'react';
import { View, Text, Button } from 'react-native';

class Profile extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text>Profile</Text>
                <Button
                    title='SIGN OUT'
                    onPress={() => this.props.navigation.navigate('SignedOut')}
                />
            </View>
        );
    }
}

export default Profile;
```

### SignIn.js

SignInボタンを押したらSignedIn画面に移動するようにします。

```js
import React from 'react';
import { View, Text, Button } from 'react-native';

class SignIn extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text>SignIn</Text>
                <Button
                    title='SIGN IN'
                    onPress={() => this.props.navigation.navigate('SignedIn')}
                />
            </View>
        );
    }
}

export default SignIn;
```

### SignedUp.js

SignUpボタンを押すとSignedIn画面へ。SignInボタンを押すとSignIn画面へ移動します。

```js
import React from 'react';
import { View, Text, Button } from 'react-native';

class SignUp extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text>SignUp</Text>
                <Button
                    title='SIGN UP'
                    onPress={() => this.props.navigation.navigate('SignedIn')}
                />
                <Button
                    title='SIGN IN'
                    onPress={() => this.props.navigation.navigate('SignIn')}
                />
            </View>
        );
    }
}

export default SignUp;
```

ここまでで移動のフローを模倣することはできましたが、実際にサインイン（管理）機構が実装されているわけではありません。
なので、実際画面がスイッチしていません。

## （模擬）認証機能の実装

では、サインイン（管理）機能を実装します。
　SignUpもしくは、（SignIn画面の）SignInボタンが押されたらサインイン済みフラグ(USER_KEY)をAsyncStorageに書き込み、SignOutボタンが押されたらflagを削除する仕様にします。

さらに、flagが存在すれば常にSignedIn画面が、flagがなければSignedOut画面がTOPページとして表示されるように実装します。

### auth.js

まず、ログイン機能を実装するauth.jsファイルをApp.jsと同じ階層に作成し、下記のように実装します。

```js
import { AsyncStorage } from "react-native";

export const USER_KEY = "auth-key";

//サインイン時
export const onSignIn = () => AsyncStorage.setItem(USER_KEY, 'true');

//サインアウト時
export const onSignOut = () => AsyncStorage.removeItem(USER_KEY);

//状態確認
export const isSignedIn = async () => {
    try{
        const _key = await AsyncStorage.getItem(USER_KEY);
        if(_key !==null){
            return true;
        }else{
            return false;
        }
    }catch(error){
        console.log(error);
    }
}
```

### App.js

サインイン状態かそうでないかにより画面をスイッチする機能を実装します。

```diff:App.js
import React from 'react';
import { View, Text, Button, SafeAreaView } from 'react-native';
import {
  createStackNavigator,
  createBottomTabNavigator,
  createDrawerNavigator,
  createSwitchNavigator,
  DrawerItems
} from 'react-navigation';

import Home from './screens/Home';
import Profile from './screens/Profile';
import SignIn from './screens/SignIn';
import SignUp from './screens/SignUp';

+import { isSignedIn, onSignOut } from "./auth";

//SignedIn時の標準画面（drawerを経由させる）
const HomeTab = createBottomTabNavigator(
  {
    Home: { screen: Home },
    Profile: { screen: Profile }
  }
);

//HomeTabを表示
const SignedIn = createDrawerNavigator(
  {
    Home: { screen: HomeTab }
  },
  {
    contentComponent: (props) => (
      <View style={{ flex: 1 }}>
        <SafeAreaView forceInset={{ top: 'always', horizontal: 'never' }}>
          <DrawerItems {...props} />
          <Button 
            title="Logout"
+            onPress={() => onSignOut().then(() => props.navigation.navigate('SignedOut'))}
          />
        </SafeAreaView>
      </View>
    )
  }
);


//SignedOut時の標準画面
const SignedOut = createStackNavigator(
  {
    SignUp: { screen: SignUp },
    SignIn: { screen: SignIn }
  }
);

//スイッチを定義
const createRootNavigator = (signedIn = false) => {
  return createSwitchNavigator(
    {
      SignedIn: { screen: SignedIn },
      SignedOut: { screen: SignedOut }
    },
    {
      initialRouteName: signedIn ? 'SignedIn' : 'SignedOut'
    }
  );
}

export default class App extends React.Component {

+  constructor(props) {
+    super(props);
+    this.state = {
+      signedIn: false,
+      checkSignIn: false
+    }
+  }

+  componentDidMount() {

+    isSignedIn()
+      .then(res => this.setState({
+        signedIn: res,
+        checkSignIn: true
+      }))
+      .catch(error => console.log(error));

+  }

  render() {
+    const { checkSignIn, signedIn } = this.state;

+    if(!checkSignIn){
+      return null;
+    }

+    const Layout = createRootNavigator(signedIn);
    return (
      <Layout />
    );
  }
}
```

### Home.js

変更なし。

### Profile.js

SignOutボタンを押すと、onSignOut()メソッドをキックした後、移動するようにします。

```diff
import React from 'react';
import { View, Text, Button } from 'react-native';
+import { onSignOut } from "../auth";

class Profile extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text>Profile</Text>
                <Button
                    title='SIGN OUT'
+                    onPress={() => onSignOut().then(() => this.props.navigation.navigate('SignedOut'))}
                />
            </View>
        );
    }
}

export default Profile;
```

### SignIn.js

SIGN INボタンを押すとonSignIn()メソッドをキックした後、移動するようにします。

```diff
import React from 'react';
import { View, Text, Button } from 'react-native';
+import { onSignIn } from "../auth";

class SignIn extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text>SignIn</Text>
                <Button
                    title='SIGN IN'
+                    onPress={() => onSignIn().then(() => this.props.navigation.navigate('SignedIn'))}
                />
            </View>
        );
    }
}

export default SignIn;
```

### SignUp.js

SIGN Upボタンを押すとonSignIn()メソッドをキックした後、移動するようにします。

```diff
import React from 'react';
import { View, Text, Button } from 'react-native';
+import { onSignIn } from "../auth";

class SignUp extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text>SignUp</Text>
                <Button
                    title='SIGN UP'
+                    onPress={() => onSignIn().then(() => this.props.navigation.navigate('SignedIn'))}
                />
                <Button
                    title='SIGN IN'
                    onPress={()=>this.props.navigation.navigate('SignIn')}
                />
            </View>
        );
    }
}

export default SignUp;
```

以上でスイッチング機能ができました。

## 見た目を整える

最後に見た目を整えてみます。

### 必要モジュールのインストール

react-native-elementsを利用したいので、インストールします。

```bash
npm install --save react-native-elements
npm install
```

### SignUp.js

フォームを設置します。

```js
import React from 'react';
import { View, Text } from 'react-native';
import { onSignIn } from '../auth';
import { Card, Button, Input } from 'react-native-elements';

class SignUp extends React.Component {
    render() {
        return (
            <View style={{ paddingVertical: 20 }}>
                <Card title="hoge">
                    <Input
                        label="Email"
                        autoCapitalize = 'none'
                    />
                    <Input
                        label="Password"
                        labelStyle={{ marginTop: 10 }}
                        secureTextEntry
                    />
                    <Input
                        label="ComfirmPassword"
                        labelStyle={{ marginTop: 10 }}
                        secureTextEntry
                    />
                    <Button
                        title='SIGN UP'
                        onPress={() => onSignIn().then(() => this.props.navigation.navigate('SignedIn'))}
                        buttonStyle={{ marginTop: 20, backgroundColor:'#03A9F4', borderRadius: 10 }}
                    />
                    <Button
                        title='SIGN IN'
                        onPress={() => this.props.navigation.navigate('SignIn')}
                        buttonStyle={{ marginTop: 40, borderRadius: 10 }}
                    />
                </Card>
            </View>
        );
    }
}

export default SignUp;
```

### SignIn.js

同じく。

```js
import React from 'react';
import { View, Text } from 'react-native';
import { onSignIn } from '../auth';
import { Card, Button, Input } from 'react-native-elements';

class SignIn extends React.Component {
    render() {
        return (
            <View style={{ paddingVertical: 20 }}>
                <Card>
                    <Button
                        title="SIGN IN"
                        onPress={() => onSignIn().then(() => { this.props.navigation.navigate('SignedIn') })}
                        buttonStyle={{ marginTop: 20, borderRadius: 10 }}
                    />
                </Card>
            </View>
        );
    }
}

export default SignIn;
```

### Profile.js

同じく。

```js
import React from 'react';
import { View, Text } from 'react-native';
import { onSignOut } from '../auth';
import { Card, Button, Input } from 'react-native-elements';

class Profile extends React.Component {
    render() {
        return (
            <View style={{ paddingVertical: 20 }}>
                <Card>
                    <Button
                        title='SIGN OUT'
                        onPress={() => onSignOut().then(() => this.props.navigation.navigate('SignedOut'))}
                    />
                </Card>
            </View>
        );
    }
}

export default Profile;
```

### Home.js

CDNから画像を落としてきて表示します。

```js
import React from 'react';
import { View, Text, ScrollView } from 'react-native';
import { Card, Button, Input } from 'react-native-elements';

const recommends = [
    { id: 1, title: 'おすすめ情報１', body: '', image: 'https://cdn.pixabay.com/photo/2015/07/27/19/44/spaghetti-863304_960_720.jpg', url: '' },
    { id: 2, title: 'おすすめ情報２', body: '', image: 'https://cdn.pixabay.com/photo/2016/06/10/01/05/hotel-room-1447201_960_720.jpg', url: '' },
];

class Home extends React.Component {
    render() {
        return (
            <View style={{flex:1}}>
                <ScrollView contentContainerStyle={{paddingVertical:20}}>
                    {
                        recommends.map(({id,title,body,image,url})=>(
                            <Card
                                key={id}
                                title={title}
                                image={{uri:image}}
                            >
                                <Text>
                                    おすすめ情報です。
                                </Text>
                            </Card>
                        ))
                    }
                </ScrollView>
            </View>
        );
    }
}

export default Home;
```

## おまけ

### Spinnerをつけてみる

ダミーですが、SignUp時の待ち時間にローディング（Spinner）をつけてみます。

```diff:SingUp.js
import React from 'react';
import { View, Text } from 'react-native';
import { onSignIn } from '../auth';
import { Card, Button, Input } from 'react-native-elements';

class SignUp extends React.Component {

+    state = {
+        spinner: false,
+    }

    render() {
        return (
            <View style={{ paddingVertical: 20 }}>
                <Card title="hoge">
                    <Input
                        label="Email"
                        autoCapitalize='none'
                    />
                    <Input
                        label="Password"
                        labelStyle={{ marginTop: 10 }}
                        secureTextEntry
                    />
                    <Input
                        label="ComfirmPassword"
                        labelStyle={{ marginTop: 10 }}
                        secureTextEntry
                    />
                    <Button
                        title='SIGN UP'
+                        onPress={async () => {
+                            this.setState({ spinner: true });
+                            await sleep(1500);
+                            this.setState({ spinner: false });
                            onSignIn().then(() => this.props.navigation.navigate('SignedIn'))
                        }}
                        buttonStyle={{ marginTop: 20, backgroundColor: '#03A9F4', borderRadius: 10 }}
                        loading={this.state.spinner}
                    />
                    <Button
                        title='SIGN IN'
                        onPress={async () => { this.props.navigation.navigate('SignIn') }}
                        buttonStyle={{ marginTop: 40, borderRadius: 10 }}
                    />
                </Card>
            </View>
        );
    }
}

export default SignUp;

+const sleep = (sec) => {
+    return new Promise(resolve => {
+        setTimeout(resolve, sec);
+    })
+}
```