はじめに

NagisaでiOSエンジニアをしている伊藤です。

今回はReactNativeというスマホアプリのクロスプラットフォーム開発ツールを使った取り組みについて紹介をしたいと思います。

アプリのクロスプラットフォーム開発といえば、以前ではTitaniumやCordovaといったWebViewをベースとしたアプリをイメージするものが多く、純粋なネイティブアプリに比べるとどうしてももっさりとした感覚になってしまい、あまり採用されることはなかったように思います。

ReactNativeではこのもっさりとした感覚はなく、とてもスムーズに動作するアプリを作れます。その理由はWebViewを使用せず、ビューやラベル、スイッチなどで本当のネイティブコンポーネントを使った画面が構成されるところにあります。これにより国内でもにわかに人気が出てきています。

UPTOON!のReactNative化

UPTOON!での課題

Nagisaではマンガ投稿プラットフォーム「UPTOON!」というサービスを運営しており、私はiOSアプリを担当しています。たくさんの作家さんが自由に作品を投稿してくださっているので、ぜひダウンロードしてお気に入りの作品に応援をしてください!


App Storeからダウンロード

今回はこのUPTOON!おいてReactNativeでの開発をスタートさせました。その理由としては作家さんからのAndroidアプリリリースの要望が強くあったことです。しかし、開発人員の確保が難しい状況があり、頭を悩ませたところReactNativeに行きつきました。ReactNativeであればiOSもAndroidも同時に開発できます。サービスとしても立ち上げたばかりということもあり、今後あるだろう頻繁なアップデートにもワンソースからiOS、Androidに展開できれば工数的にも非常にメリットがあると思い、未経験ながら挑戦してみることにしました。

ReactNativeについて

概要

改めてReactNativeについてですが、Facebookが開発したアプリ開発ツールで、JavaScriptライブラリのReact.jsの技術を応用しています。よって開発言語の主体はJavaScriptになり、フロントエンドエンジニアにとっては入りやすいと思います。逆にネイティブアプリエンジニアにとっては若干ハードルがあり、普及しにくい要因になってるのではないでしょうか。

導入について

導入に必要な作業については以下の通りです。

  • Node.jsのインストール
  • ReactNativeのインストール
  • Xcode、AndroidStudioのインストール
  • テキストエディタの用意

ReactNativeはnpmパッケージとして提供されているで、事前にNode.jsのインストールが必要です。また、動作確認する際にはシミュレータを起動するので、iOSならばXcode、AndroidならばAndroidStudioをインストールしている必要があります。

あとはお気に入りのテキストエディタを用意してプログラミングを開始します。私は普段Xcodeのエディタ以外まともに使っていませんが、今回はAtomを使って開発しています。

他にもexpoというサービスではswiftなどのネイティブコードが一切さわれなくなることと引き換えに、簡単に開発を始められたり、QRコードでアプリ配布ができたりもします。

作ってみた

技術的な説明についてはそこそこに、現在開発中ではありますが、JavaScriptコードから出来上がったiOS、Androidの漫画アプリを見ていただければと思います。



プラットフォーム毎に多少異なるネイティブコンポーネントが使われつつも、全体的には同じ動きをするアプリケーションに仕上がっていることが確認できるかと思います。

ここまでに至る開発での所感については下記の通りです。

良かった

後ほど解説しますが、UI作成は非常にスムーズに進めることができました。またiOSとAndroidが同時出来上がっていく様子はとても楽しいものです。デバッグではソースコードを変更後、ビルドなしでシミュレータへ変更を反映できるHotReloadingや、ソースコード保存と同時に反映が行われるLiveReloadによってデバッグを進めることができます。ObjcからSwiftに移行してビルド時間が伸びたと嘆くiOSエンジニアも多くいるかと思うのでこの機能は非常に助かります。またUIを実装していく上で便利なライブラリがたくさんあり、この辺りは困ることがありませんでした。

悪かった

エラーが発生すると画面が真っ赤になり問題のソースコードの行数などが表示されたりしますが、時折直接的に分からないエラーメッセージが出ることがあり、問題箇所の調査に時間がかかってしまうことがしばしばありました。こちらは慣れてくれば勘が働いてくると思うので我慢が必要です。また、こちらも後ほど解説しますが、ビジネスロジックの実装については戸惑いがありました。暗号化やZipアーカイブなど難易度が高い処理についてはライブラリも十分と言える状況ではなく、対応には苦労がありました。

技術解説

ここではReactNativeについてより詳しく理解して頂けるよう、アプリ開発を通じて得た技術的知見について解説をしたいと思います。実際にコードへ落とし込みまでイメージできるように、サンプルアプリを用いてソースコードを交えて説明していきます。

サンプルアプリの概要

サンプルアプリは1画面のみのリスト表示を行うもので、ヘッダービューに配置された+ボタンをタップすると現在時刻がリストに追加され、ゴミ箱ボタンをタップするとリストがクリアされる、という単純なものです。

画面の実装

React

ReactはMVCで言う所のViewのライブラリ、という記述をよく見かけますが、実際にその通りでJSXというHTMLのようなマークアップ言語とCSSライクなスタイル定義を駆使して画面を作成します。Web開発者にとっては当たり前ですが、XcodeのxibやstoryboardのようなGUIを使わず、全てコーディングで書く開発は慣れてくると非常に効率的です。

NativeBase

さらに今回はNativeBaseというネイティブのUIコンポーネントを再現してくれるライブラリを使用します。ReactNative自体も様々なコンポーネントを用意してくれていますが、iOSのみやAndroidのみのものも多いので、こちらを使用するとより効率的に開発ができます。

Component

それではこれらを使って先ほどのスクショのようなUIを作ると下記のようなコードになります。

import React, { Component } from 'react'
import { Container, Header, Left, Right, Button, Icon, Content, List, ListItem, Text } from 'native-base'

class ListSample extends Component {
  render() {
    const { clear, add, items } = this.props
    return (
      <Container>
        <Header>
          <Left>
            <Button transparent onPress={clear}>
              <Icon name='trash' />
            </Button>
          </Left>
          <Right>
            <Button transparent onPress={add}>
              <Icon name='add' />
            </Button>
          </Right>
        </Header>
        <Content>
          <List dataArray={items} renderRow={(item) =>
              <ListItem>
                <Text>{item}</Text>
              </ListItem>
          }/>
        </Content>
      </Container>
    )
  }
}

画面を実装する際はReactのComponentを継承して、render()メソッド内にJSX記法で画面構成を表現します。this.propsはListSampleが外部から受け取るプロパティになります。上記のclearとaddはメソッド、itemsは配列データとして受け取ることを想定しています。

CSS、Flexbox

JSXではHTML同様に画面の構成のみの表現なので、色や大きさ、配置などのスタイルに関してはCSS、Flexboxでの記述が必要です。ここでは詳細な説明について割愛しますが、既存のWeb技術と同様ですので情報は豊富にあり困ることはないかと思います。

ビジネスロジックの実装

Redux

ReactはViewの実装のみのサポートなので、ビジネスロジックやデータ管理の実装方法については考慮が必要です。相応の規模のアプリケーションを開発するならばReduxというフレームワークを選択することになると思います。ReduxはReactと非常に親和性が高く、関連するライブラリも豊富にあります。Action、Reducer、Storeといった機能に分割されており、データの流れを1方向にすることでシンプルで明快な設計にしてくれます。

Action

ユーザー入力やイベント開始、終了といったActionを定義します。オブジェクト内にtypeキーでActionのIDを持つことが必須になります。また、Actionに付随するデータを含めることが可能です。

サンプルでは日付の追加(addDate)と日付リストの消去(clearDate)を定義しています。

export const addDate = () => {
  console.log('action')
  let date = new Date()
  return {
    type: 'ADD_DATE',
    date: date.toString(),
  }
}
export const clearDate = () => {
  console.log('action')
  return {
    type: 'CLEAR_DATE',
  }
}

Reducer

Reducerは発行されたActionによりデータを更新します。このデータをStateと言います。Reducerで扱うデータがStoreに保持されることになるため、役割的にはModelやEntityといったクラスに近いイメージかなと思います。

サンプルではActionから受け取った日付文字列を配列にセットしたり、配列をクリアしたりします。

export const date = (state = {items: []}, action) => {
  console.log('reducer')
  switch (action.type) {
    case 'ADD_DATE':
      let items = Object.assign([], state.items)
      items.push(action.date)
      return {...state, items: items}
    case 'CLEAR_DATE':
      return {...state, items: []}
    default:
      return state
  }
}

Store

Storeは各Stateを保持し、対応するComponentにStateの更新を伝えます。また様々なMiddlewareを追加することができ、これにより機能を拡張します。

サンプルではredux-loggerというMiddlewareを追加しており、これにより発行されたActionやStateの変化をログとして確認できるようになります。詳細は後述する実行結果の項目に記載します。

import { createStore, applyMiddleware } from 'redux'
import logger from 'redux-logger'
import { date } from '../reducers'

export const store = createStore(date, applyMiddleware(logger))

生成されたStoreをReactのComponentと接続する必要があり、下記の通り実装します。

import React, { Component } from 'react'
import { Provider } from 'react-redux'
import ListSample from './app/components'
import { store } from './app/store'

export default class App extends Component<{}> {
  render() {
    return (
      <Provider store={store} >
        <ListSample />
      </Provider>
    )
  }
}

Appはアプリケーションの最上位のComponentです。この直下にProviderを組み込みStoreをつなぎます。

Container

最後に各画面のComponentとAction、Stateをつなぎます。

import { connect } from 'react-redux'
import { addDate, clearDate } from '../actions'

export default connect(
  state => ({
    items: state.items,
  }),
  dispatch => ({
    add: () => dispatch(addDate()),
    clear: () => dispatch(clearDate()),
  })
)(ListSample)

connectメソッドによってつながれたものをContainer Componentと言い、Providerの配下に置くことでStateの更新を受け取れるようになります。これに対しconnect以前のものはPresentational Componentと言われます。Stateが更新されるとPresentational Componentのrenderメソッドがコールされるようになります。

実行結果

それでは実行してみます。ReactNativeではconsole.log()で出力したログをブラウザで確認することができます。StoreのMiddlewareで追加したredux-loggerの出力もここで確認できます。

スクショは画面のヘッダービューの+ボタンをタップした時の様子です。

action → reducer → store(redux-loggerの出力) → renderと順次実行された様子が確認できるかと思います。

またredux-loggerのprev stateとnext stateを比較するとitemsの配列にactionのdateの値が反映された様子も見て取れます。

実際に画面も更新されて、リストに日付文字列が追加されています。

 

解説は以上になりますが、今回作成したサンプルコードはGithubで公開していますので、cloneした後にnpm installをして頂ければ実際の動作を確認できるかと思います。

補足

サンプルアプリでは実装されていませんが、実際の開発では通信などで非同期処理が必要になる場面があるかと思います。しかし、Reduxでは同期処理しか対応してないので別途ライブラリを使用する必要があります。私はredux-sagaというライブラリを導入しましたが、こちらもその概念理解など大変でした。他にもデータの永続化などライブラリに頼る場面が多々あり、また次回この辺りの紹介もできればなと思います。

さいごに

元々iOSエンジニアでまともにWeb開発の経験がない自分にとって慣れないJSやフロント開発のお作法的なところで戸惑いもありましたが、慣れてくれば非常に快適でした。ReactNativeを通して少しですがフロント開発についても理解が進みますし、技術の幅が広がるのはとても良いことだと思います。ぜひ皆さんも挑戦していただけたらなと思います。

また弊社の全iOSエンジニアが総出で社内ノウハウを発信するSwift勉強会「Nagisa.swift」も開催するのでご興味ある方は下記よりご応募ください!

Nagisa.swiftを見てみる