123
102

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.

React NativeAdvent Calendar 2016

Day 21

React Native Router Fluxを使ってみませんか・・・?

Posted at

rnrf使ってる?

rnrf(react-native-router-flux)はreact routerライクにreact nativeのルーティングを行ってくれるライブラリになります。

react nativeのルーティングライブラリとしては人気があるのですが、いかんせん公式ドキュメントが不親切なような気がしています・・・・。
サクッとルーティングを行えることがこのライブラリの素晴らしさだと思うのですが、うまく動かない・・・・。といって苦戦する場面も多くありました。

結構トラウマだったりします。そこで、苦戦した場面とそのソリューションを共有することで快適なrnrfライフ、もといReact Nativeライフを送って頂けますと幸いです。

先日開催されたReact Native meetup忘年会でも「つらい!簡単にやる分にはいいけどつらい!」という話もでました。僕もつらかったです。
ですが、使う価値も十分あると思うので紹介させて頂きます・・・!!。

はじめに

2つパターンとナビゲーションの実装を紹介いたします。このパターンを元にrnrfの使い方をお伝えできたらと思います。

基本パターン

遷移して遷移して・・・・・・元の画面に戻るというパターンです。

よくあるパターン1.png

基本的なこと

pageA~Cの3つのコンポーネントを用意しました。
これをpageA -> pageB -> pageC と遷移させます。

まずはルーティングを行うためにapp.jsに色々と書いていきます。

app.js
import React from 'react';
import {
    Scene,
    Router,
} from 'react-native-router-flux';
import {
    PageA,
    PageB,
    PageC,
} from './component';


const App = () => (
    <Router>
        <Scene key="root">
            <Scene key="pageA" initial component={PageA} title="PageA" />
            <Scene key="pageB" component={PageB} title="PageB" />
            <Scene key="pageC" component={PageC} title="PageC" />
        </Scene>
    </Router>
);

export default App;

Routerで囲って、Sceneで定義していきます。重要なこととしては「keyがシーンの識別子になる」ということのみです。

次のページに行こまい

次のページへ行くためにはActions[key]を使います。

pageA
import React from 'react';
import {
    View,
    Text,
    TouchableOpacity,
    StyleSheet,
} from 'react-native';
import {
    Actions,
} from 'react-native-router-flux';


const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
    },
    linkText: {
        fontSize: 32,
        color: 'rgb(95, 177, 237)',
    },
});

const PageA = () => (
    <View style={styles.container}>
        <TouchableOpacity onPress={Actions.pageB}>
            <Text style={styles.linkText}>Link</Text>
        </TouchableOpacity>
    </View>
);
export default PageA;

これで次のページいけます。
何か処理をさせてから、やっていきたいときは

・・・・処理

   Actions.pageB();

・・・・処理

とメソッドを実行すれば大丈夫です。
ポイントとしては先ほど定義したkeyを使っているという点です。

戻る挙動について

以上の実装だけで自動でスワイプバックと戻るボタンが付いてきます。
qiita_01.gif

わかりにくくて申し訳ないのですがちゃんとシーンが積み重なっているというスマートフォン特有の戻り方をしてくれます。

次のページへ

同じようにPageBでpageCに遷移できるようにしていきます。

import React from 'react';
import {
    View,
    Text,
    TouchableOpacity,
    StyleSheet,
} from 'react-native';
import {
    Actions,
} from 'react-native-router-flux';

const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
    },
    linkText: {
        fontSize: 32,
        color: 'rgb(95, 177, 237)',
    },
});

const PageB = () => (
    <View style={styles.container}>
        <TouchableOpacity onPress={Actions.pageC}>
            <Text style={styles.linkText}>Link</Text>
        </TouchableOpacity>
    </View>
);
export default PageB;

本題

ここでHomeを押してPageAに戻らせるためには、Actions.pageA()で大丈夫なのですが、これではpageCの上にpageAがのって・・・という無限ループに差し掛かります。そしてこれは画面遷移であって戻る挙動ではありません。

ここでActionConstとtypeを使うことでこれを回避することができます。

PageC.js
import React from 'react';
import {
    View,
    Text,
    TouchableOpacity,
    StyleSheet,
} from 'react-native';
import {
    Actions,
    ActionConst,
} from 'react-native-router-flux';

const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
    },
    linkText: {
        fontSize: 32,
        color: 'rgb(95, 177, 237)',
    },
});

const PageC = () => (
    <View style={styles.container}>
        <TouchableOpacity onPress={() => { Actions.pageA({ type: ActionConst.REFRESH }); }}>
            <Text style={styles.linkText}>Home</Text>
        </TouchableOpacity>
    </View>
);
export default PageC;

ちょっとonPressの書き方がアレですが、typeにActionConstを指定することで色々な動きを指定することできます。中にはドキュメントに乗っていないものもあったり全て把握している訳ではないのですが、理解している分について説明いたします。

Actions.pageA({ type: ActionConst.REFRESH });

とすることで、ダブっている箇所まで戻り、その間の履歴を消すことができます。
ActionConst.RESETも似たような動きをしますが、こちらは過去履歴を全て削除する方法になっています。

RESETとREFRESHの挙動の違い

少しわかりにくい彼らなのですが、pageCのonPressを下記のようにして挙動を見てみるとわかりやすくなります。

RESET

Actions.pageB({ type: ActionConst.RESET });

REFRESH

Actions.pageB({ type: ActionConst.REFRESH });

注意点は先ほどはpageAだったのがpageBになっている点です。

RESET REFRESH
qiita_RESET.gif qiita_REFRESH.gif

ここで大切な点としては、

RESETが全ての因果を断ち切ると言わんばかりに過去履歴をぶっ飛ばすのに対して、REFRESHはちゃんとpageBに戻ってくれてpageAの履歴を保っているということです。

タブパターン1

基本パターンに常にタブが表示されているパターンです。タブが表示されていてもタブが常に表示されているままなのであれば先ほどと同じパターンなのですが、タブがあったりなかったりするとちょっと都合が変わってきます。
ここでは、タブ内の画面から、タブがない画面に遷移するパターン。そしてフィニッシュはタブの起点に戻ります。

よくあるパターン2.png

タブの実装について

タブを実装するためにはtabsとtabIconを利用します。
自分がやるときにはタブの実装についてはこちらの記事を参考にしましたので紹介させていただきます!!
React Native Routing について

タブ画面の遷移実装は下記のようになります。
タブ内の画面はPageA, PageA2, PageA3です。

app.js
import React from 'react';
import {
    StyleSheet,
} from 'react-native';
import {
    Scene,
    Router,
} from 'react-native-router-flux';
import {
    PageA,
    PageA2,
    PageA3,
    PageB,
    PageC,
} from './component';
import TabIcon from './TabIcon';

const styles = StyleSheet.create({
    tabBar: {
        flex: 1,
        backgroundColor: 'rgb(50, 207, 202)',
    },
});

const App = () => (
    <Router>
        <Scene key="root">
            <Scene
              key="tabbar" tabs
              tabBarStyle={styles.tabBar}
            >
                <Scene key="pageA" initial component={PageA} title="PageA" icon={TabIcon} />
                <Scene key="pageA2" component={PageA2} title="PageA2" icon={TabIcon} />
                <Scene key="pageA3" component={PageA3} title="PageA3" icon={TabIcon} />
            </Scene>
        </Scene>
    </Router>
);

export default App;
tabIcon
import React from 'react';
import {
    View,
    Text,
    StyleSheet,
} from 'react-native';

const styles = StyleSheet.create({
    tabText: {
        color: 'white',
    },
    tabTextActive: {
        color: 'gray',
    },
});

const TabIcon = props => (
      <Text
        style={
          props.selected ?
          styles.tebTextActive :
          styles.tabText
        }
      >
          {props.title}
      </Text>
);

export default TabIcon;

iconに指定したコンポーネントにはprops.selectedが流れてくるのでアクティブかどうかはそれを元に判定すればokです。
また、今回はprops.titleを表示させていますがそれぞれ別の表示にしたい場合はそれぞれのコンポーネントを作成することでできます。

タブ外画面に遷移する

ここからが曲者です。
タブ内の画面からタブ外の画面に遷移するためには、タブ外にSceneを配置します。

app.js
import React from 'react';
import {
    StyleSheet,
} from 'react-native';
import {
    Scene,
    Router,
} from 'react-native-router-flux';
import {
    PageA,
    PageA2,
    PageA3,
    PageB,
    PageC,
} from './component';
import TabIcon from './TabIcon';

const styles = StyleSheet.create({
    tabBar: {
        flex: 1,
        backgroundColor: 'rgb(50, 207, 202)',
    },
});

const App = () => (
    <Router>
        <Scene key="root">
            <Scene
              key="tabbar" tabs
              tabBarStyle={styles.tabBar}
            >
                <Scene key="pageA" initial component={PageA} title="PageA" icon={TabIcon} />
                <Scene key="pageA2" component={PageA2} title="PageA2" icon={TabIcon} />
                <Scene key="pageA3" component={PageA3} title="PageA3" icon={TabIcon} />
            </Scene>
            <Scene key="pageB" title="PageB" component={PageB} />
            <Scene key="pageC" title="PageC" component={PageC} />
        </Scene>
    </Router>
);

export default App;

pageA, pageA2, pageA3からタブがないpageBへ遷移させるようにしたいと思います。
そして、遷移するときはいつも通り

Actions.pageB();

でOKなので、pageA、pageA2, pageA3は下記のようにします。

pageA.js
import React from 'react';
import {
    View,
    Text,
    TouchableOpacity,
    StyleSheet,
} from 'react-native';
import {
    Actions,
} from 'react-native-router-flux';


const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
    },
    linkText: {
        fontSize: 32,
        color: 'rgb(95, 177, 237)',
    },
});

const PageA = () => (
    <View style={styles.container}>
        <TouchableOpacity onPress={Actions.pageB}>
            <Text style={styles.linkText}>Link</Text>
        </TouchableOpacity>
    </View>
);
export default PageA;

これでpageA, pageA2, pageA3からpageBに行くことができます。
そして、戻るを押したときはタブのフォーカスを持ったまま戻ることができます。

qiita_tabs.gif

タブ付きの画面に戻る

react-native-router-fluxがうざくなってくるのがここらへんからです。
いままでの感覚だと、タブ付きの画面からタブ内のpageA2に遷移して、pageB, pageCに遷移した後に戻るためには、

Actions.tabbar({ type: ActionConst.REFRESH });

を使いたくなります。

pageC.js
import React from 'react';
import {
    View,
    Text,
    TouchableOpacity,
    StyleSheet,
} from 'react-native';
import {
    Actions,
    ActionConst,
} from 'react-native-router-flux';

const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
    },
    linkText: {
        fontSize: 32,
        color: 'rgb(95, 177, 237)',
    },
});

const PageC = () => (
    <View style={styles.container}>
        <TouchableOpacity onPress={() => { Actions.tabbar({ type: ActionConst.REFRESH }); }}>
            <Text style={styles.linkText}>Home</Text>
        </TouchableOpacity>
    </View>
);
export default PageC;

これで無事に戻ってこれるようになります。
しかし、ここで問題が発生します。

元に戻ってきた後画面遷移しようとするとシーンキーが重複してるよというエラーがでて画面真っ赤っかになります。

qiita_tabs_error.gif

push_or_pop

ここで救世主PUSH_OR_POPを使います。

Actions.tabbar({ type: ActionConst.PUSH_OR_POP });

これによって無事に画面真っ赤かにならないようになります。

page.js
import React from 'react';
import {
    View,
    Text,
    TouchableOpacity,
    StyleSheet,
} from 'react-native';
import {
    Actions,
    ActionConst,
} from 'react-native-router-flux';

const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
    },
    linkText: {
        fontSize: 32,
        color: 'rgb(95, 177, 237)',
    },
});

const PageC = () => (
    <View style={styles.container}>
        <TouchableOpacity onPress={() => { Actions.tabbar({ type: ActionConst.PUSH_OR_POP }); }}>
            <Text style={styles.linkText}>Home</Text>
        </TouchableOpacity>
    </View>
);
export default PageC;

PUSH_OR_POPは履歴を探して、同じ履歴があればpush、なければpopで元の画面に戻るというtypeです。これを使うことで無事に戻ることができます!

qiita_tabs_pop.gif

タブからタブ外へでて戻ろうとするとキーのネーミングが複雑なためREFRESHでは解決できなくなります。
そこで、ただ純粋にpopするPUSH_OR_POPを使うことで解決できます。

# 背景画像の取り扱いについて

ナビゲーション、タブともに背景に画像を使えるのですが複数デバイス対応を狙うなら使わない方がいいかもです。基本的にはassetsが使えれば楽になんとかなりますが、iOS, Androidのことを考えるとそれはできないので、頑張るしかありません。
特にimageにstyleを付与できない事案に阻まれることになるため、背景画像を引き延ばして使うなんてことができません・・・・。

背景画像を敷く

あえて、ナビゲーションを背景画像として用いるには適切でない正方形の画像を用意しました。

MyBack.png

これを引き延ばして設置して見ます。

navigationBarBackgroundImage

これを使えば良さそうなのですが、

NavBar.js
        {navigationBarBackgroundImage ? (
          <Image source={navigationBarBackgroundImage}>
            {contents}
          </Image>
        ) : contents}

rnrfのソースはこうなっており、Imageのstyleをなんとかすることができません。
ですのでどれだけ頑張ろうともImageを変更することができず、

miss.png

こんなことになってしまいます。

android, iOS対応のstatic Imageを使おうとすると否応無しにどうしていいのかわからなくなります。

そのまま使いこなすには?

もちろん、ここでrnrfの中身を変えてやればうまくいきます。しかし、できればそんなことはせずになんとかしたいかと思います。
そのまま使いこなすには、navBarを使うことができます。
navBarにはコンポーネントを指定します。

指定するコンポーネントは

const NavBar = (props) => (
    <View style={styles.container}>
        <Image
            source={NAV_BACKGROUND}
            style={styles.navImage}
        >
            <View style={styles.left}>
                <TouchableOpacity onPress={Actions.pop} >
                    { props.hideBackImage ? null : (<Text style={styles.leftButton}>  {'<'} </Text>) }
                </TouchableOpacity>
            </View>

            <View style={styles.main}>
                <Text style={styles.title}>{props.title}</Text>
            </View>

            <View style={styles.right}>
                <TouchableOpacity onPress={props.onRight}>
                    <Image
                        source={props.rightButtonImage}
                        style={styles.rightButtonImage}
                    />
                </TouchableOpacity>
            </View>
        </Image>
    </View>
);

export default NavBar;

とこのようにします。ものすごく簡易実装なので、全てのAPIを有意義に使うことはできない状態ですので、ちゃんと実装するにはもう少し手を加える必要があります。

ポイントとしては、Sceneコンポーネントに指定したのもは全てprops[propName]を通じてアクセスすることができます。navStateもいけるのでガチ実装したいときはそれも使うことができます。
ちなみに、余談ではありますが

const styles = StyleSheet.create({
    container: {
        height: 64,
        width: Dimensions.get('window').width,
        position: 'absolute',
        top: 0,
    },
    navImage: {
        flex: 1,
        width: undefined,
        height: undefined,
        flexDirection: 'row',
    },
});

このようにすることでこの正方形の画像を引き延ばすことができます。

success.png

しかし、もともと正方形だけあって画像が荒くなってしまいますね・・・・・。

タブの背景画像について

諦めろとしか言いようがありません。デバイスが限られているならありかもしれませんが、複数端末を画像の伸縮によって行おうとするとどうにも対処できません。タブ画像にstyleを適用できない仕様になっているためです(どうあがいても不可能)。なので、どうしても使いたい場合はforkしてオレオレカスタマイズするよりありません。オレオレカスタマイズで乗り切りました・・・・!!。

なんせrnrf側の実装はこうなっているので・・・・。

        {!hideTabBar && state.children.filter(el => el.icon).length > 0 &&
          (state.tabBarBackgroundImage ? (
            <Image source={state.tabBarBackgroundImage}>
              {contents}
            </Image>
          ) : contents)
        }

これについては辛すぎるのでそろそろPR送ってみたいと思います。反映されればこれもクリアーできるはず・・・・!!!

## 終わりに

色々やってみてうまく行くと「めっちゃ楽!!!」と思うのですがうまくいかないと「なにやってんだろ・・・」ってなります。
ここでやったことに関しては問題なくできているので、ここでやったような実装方法については「めっちゃ楽!!」と思っていただけるかと思います。
お役に立てますと幸いです。

123
102
3

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
123
102

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?