React NativeでTodoアプリを作ってみた

2015-06-08

前回の初心者swiftに引き続き、React Nativeも面白そうだから触ってみようということで、Todoアプリを作ってみました。

もちろんReactjsも触ったことなかったので、流行りのFluxとかは二の次です。

完成形はこんな感じ

todo1
todo3 todo2 todo4

Todoをタップするとアラートが出て、Doneにしたり消せたりできます。

タブでTodoとDoneを切り替えています。ただ、ストレージに保存してないので再起動したら消えちゃうんですけどね、、、

ソースはこちら

https://github.com/inkenkun/react-native-todo

React Nativeをインスコ

react-nativeのサイトの通りに入れます。

$ npm install -g react-native-cli
$ react-native init プロジェクト名

init するとxcodeのプロジェクトファイルとか勝手に作ってくれます。

まずはプロジェクトファイルを開いて、そのまま実行してみます。

こんなターミナルが立ち上がって、次にシミュレータが起動しますね

todo7

このシミュレーターに書いてある通り、ReactNativeはソースを書き換えたら、Cmd+Rするだけで反映するのでさくさく開発できちゃいます

エラーもシミュレータ上にこんな感じで出るのでわかりやすいです。

todo8

 

Reactjsファイルを作る

では早速 reactjs ファイルを作って行きましょう。

今回はこんな感じで4つのコンポーネントに分けました

todo9

大枠のTodoReactがあって、そこからヘッダ、Todoリストの塊を呼んでます。

Todoリストはさらにその中で、ひとつひとつのTodoItemを呼んでます。

フォルダとファイル構成はこんな感じ

todo5

Appフォルダは自分で作って、その下にReactjsのファイルを入れました。

index.ios.jsを編集

index.ios.jsが一番最初にネイティブから呼ばれるファイルです。

initした直後だとこれに全部書いてあるので、これを以下のようにして、ガワのTodoReact.react.jsを呼ぶようにします。


'use strict';

var React = require('react-native');

var TodoReact = require('./App/components/TodoReact.react');

var {
AppRegistry,
} = React;

AppRegistry.registerComponent('TodoReact', () => TodoReact);

これで最初にTodoReact.react.jsが呼ばれるようになりました

TodoReact.react.js

今回メインのファイルです。

Todoを保存したり消したりアラートやタブを設定したりしてます。

最初なんもないとさみしいので、getInitialStateでデフォルトTodoを設定しています。

getInitialState: function() {

    return {
      items: [
                {id: 1, todo: 'Learn react native', complete: false},
                {id: 2, todo: 'Make a to-do app', complete: true}
            ],
      value: '',
      selectedTab: 'todo'
    };
  },

completeがfalseだとTodoでtrueだとDoneとしています。

アラートメニュー

react-nativeだとまだリストを横にスワイプして消すとか、ボタンとかなさそうなので、どうしたらいいのかなーって色々サンプル見てたら、Todoをタップしてアラートメニューを出してやってたのでそれをまるまる参考にしました。

あとは、本家ではないけど、スワイプして消すやつを作ってくれてる人もいるみたい↓

react-native-swipeout (https://github.com/dancormier/react-native-swipeout)

alertMenu: function(rowData, rowID) {
      AlertIOS.alert(
          'どうする?',
          null,
          [
              {text: 'Doneにする', onPress: () => this._done(rowData)},
              {text: '消す', onPress: () => this._del(rowData)},
              {text: 'Cancel'}
          ]
      )
  },

アラートメニューは配列でメニューを設定して、そのメニューに対して関数を実行する感じですね。

タブバー

タブバーではiOSの標準のアイコンを使うこともできますが、テキストが変えられないので、アイコンごと変えちゃいましょう

icon8でアイコン取ってきて、xcodeのImages.xcassetsに設定します。

todo10

こればかりは、Cmd+Rで画像を呼べるようにならないので、シミュレータを起動してる場合は、一度Stop して再度起動します。

そうするとReactjsから画像ファイルが扱えるようになります。

<TabBarIOS
        tintColor="black"
        barTintColor="#3abeff"
        style={styles.tab}>

        <TabBarIOS.Item
          selected={this.state.selectedTab === 'todo'}
          title="Todo"
          icon={{uri :'List'}}
          onPress={() => {
              this.setState({
                  selectedTab: 'todo',
              });

iconのuriで Images.xcassetsのファイルを指定してます

関数いろいろ

_getTodo: function(items){
  	  return items.filter(function(v){
	    return v.complete === false;
	  });
},
_getDone: function(items){
  	  return items.filter(function(v){
	    return v.complete === true;
	  });
},

_getTodoでTodoのリストを、_getDoneでDoneのリストを取ってきてます。

  _save: function(event) {

  	var text = [ {id: todoId(), todo: event.nativeEvent.text, complete: false }]

    this.setState({
      items: this.state.items.concat(text),
      value: ''
    });
  },

  _done: function(rowData) {

  	var items = this.state.items.filter(function(v){
	    if (v.id == rowData.id){
	    	v.complete = true;
	    }
	    return v
	  });

  	this.setState({items: items})
  },

  _del: function(rowData) {
      var items = this.state.items;

      items = items.filter(function(v){
	    return v.id != rowData.id;
	  });
      this.setState({items: items})
  }

_save:新規登録 _done:Todoを実行済みにする _del:消す です。

setStateすることによって、自動的にrenderが再度走り、DOMが再構成されるんですね。

TextInput

<TextInput
	   style={styles.todoInput}
	   id={this.props.id}
	   value={this.state.value}
	   onChangeText={(text) => this.setState({value: text})}
	   onBlur={(event) => this._save(event)} />

valueが初期値なんですが、getInitialStateで「value:”」にして、_saveしたときに「value:”」にするだけだと、状態の変化が取得できないのか、_saveした時に空にならず入力値が入ったままになってしまいました。なので、onChangeTextでsetStateするようにしました。

TodoList.react.js

Todoリスト(Doneリストも)のかたまりを扱うコンポーネントです。

getInitialStateで親のTodoReact.react.js からTodoリストを受け取ってます。

componentWillReceiveProps : function(next){

  this.setState({
    dataSource: this.ds.cloneWithRows(next.items),
  });
},

componentWillReceivePropsは propsの値が変わった時に実行されます。

ここでsetStateしてるので、親から渡ってきた値が変わったらTodoList.react.jsも再renderされるんですね。

TodoItem.react.js

Todo(Done)リストの一つ一つのItemです。

<TouchableOpacity
          onPress={this.props.onPress} >
    <View style={styles.row}>
       <Text style={styles.memo}>
         {item.todo}
       </Text>
       <View style={styles.separator} />
    </View>
</TouchableOpacity>

TouchableOpacityでタップイベントを発生させてますね。

this.props.onPressはTodoReact.react.jsから渡ってきた関数です。

ここでは、TodoReact.react.jsで「onPress={this.alertMenu}」って指定していて、TodoList.react.jsで、TodoItemにそのまま渡してるので、alertMenuが実行されます。

Header.react.js

ヘッダコンポーネントです。

<Image source={{uri: 'https://avatars1.githubusercontent.com/u/1897172?v=3&s=460'}}
         style={styles.logoImg} />

こんな感じで、どこかのサイト上の画像を表示するのも簡単です。

実機で確認

実機で確認する場合は、AppDelegate.m に書いてある通り、プロジェクトディレクトリで以下を実行します。

$ react-native bundle --minify

そして、 AppDelegate.mの該当部分を変更します。

jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle"];

↓

// jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle"];
// jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];

↓

jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];

完成

Cmd+Rですぐコードが反映されるので、ほんとさくさく開発できていいですね。

レイアウトなどの見た目を、CSS(っぽいもの)でやるのでその辺が大変だなと思いました。思ったように表示してくれない、、みたいな。

本家以外にもnpmでいろんな機能をいろんな人が作っているので、これからもっとネイティブに近いことができるようになるんだろうなって思います。

参考ソース

https://github.com/dancormier/react-native-swipeout

https://github.com/hartmamt/TodoMVC-react-native