LoginSignup
6

More than 1 year has passed since last update.

React Native + TypeScript で iOS アプリのひな形を作る

Last updated at Posted at 2020-12-22

こんにちは。Japan Digital Designでインフラチームに所属している猪熊です。JDDでは、有志で AdventCalendar をやっています。本記事は JDD Advent Calendar 22日目です。よろしくお願いいたします。

とあるきっかけでスマホアプリ開発を手伝うことになりました。これまでサーバサイド、インフラメインで業務してきたのでフロントエンドやネイティブアプリ開発経験はほとんどありません(5年前くらいに Socket.io と Node.js で簡単なチャットアプリ作ってみた程度)。
今回はプロジェクトの雛形を作るまでにやったことを共有します。
私のようなフロントエンド・ネイティブアプリ開発経験のないエンジニアの方の参考になれば幸いです。

React Nativeにした理由

ゆくゆくはiOSだけでなくAndroidもリリースしたいものの、2つ開発する余力がなく、クロスプラットフォームを選んでいます。上に書いたように JavaScript の経験は多少あったのでReact Nativeでやってみようということになりました。

※社内で投稿を確認してもらっていて知ったのですが、特にモバイル開発者未経験者には、Expo CLIを使った開発を推奨しているそうです。今回の記事には含まれませんが、そちらも検討してみても良いかも知れません。

React Native + TypeScript で iOS アプリのひな形を作る

さて、ここからが本編です。

開発環境構築編

まずは開発環境を構築します。前提条件は以下です。

  • 使用端末: MacBook Pro / macOS Catalina 10.15.7
  • 使用エディタ: Visual Studio Code

環境構築に関する情報は検索すると沢山出てきます。
今回は詳細は割愛して、インストールしたものの列挙に留めます。

VSCode Extensions

  • ESLint
  • Prettier
  • EditorConfig for VSCode
  • Jest

実行環境・開発ツール系

  • Node.js(私は anyenv 経由でインストールしました)
  • yarn
  • watchman
  • Xcode
  • CocoaPods

雛形プロジェクト作成編

まずは、React Native のプロジェクトを作成します。

npx react-native init RNSampleApp --template react-native-template-typescript

作成が終わったらアプリが起動することを確認します。

cd RNSampleApp
npx react-native run-ios

package.json を編集する

次に package.json を編集します。

name を変更する

RNSampleApp のままだと、警告が表示されるので適当に変更してきます。
ここでは、 rn-sample-app としておきます。

パッケージバージョンの^ (ハット)を削除する

例えば、以下のようにバージョン番号の前に ^ がついているものがあります。

  "devDependencies": {
    "@babel/core": "^7.8.4",
    "@babel/runtime": "^7.8.4",
    "@react-native-community/eslint-config": "^1.1.0",
    "@types/jest": "^25.2.3",
    "@types/react-native": "^0.63.2",
    "@types/react-test-renderer": "^16.9.2",
    "@typescript-eslint/eslint-plugin": "^2.27.0",
    "@typescript-eslint/parser": "^2.27.0",
    "babel-jest": "^25.1.0",
    "eslint": "^6.5.1",
    "jest": "^25.1.0",
    "metro-react-native-babel-preset": "^0.59.0",
    "react-test-renderer": "16.13.1",
    "typescript": "^3.8.3"
  }

先頭に ^ がついているパッケージは、メジャーバージョンしか固定されません。
yarn コマンド実行時に常に最新のマイナーバージョン、パッチバージョンのものがインストールされるため、記載されているバージョンとは異なるパッケージがインストールされる可能性があります。
(特にチーム開発をしている場合は)環境差異によるトラブルを避けるためにも削除しておいた方が良いと思います。

Prettierの設定を行う

Prettierを追加することで、常にコードフォーマットされるようになります。

yarn add --dev --exact prettier

.prettierrc.yaml をプロジェクトルートディレクトリ直下に作成します。

endOfLine: lf
tabWidth: 2
semi: true
singleQuote: true
trailingComma: all

overrides:
  - files: '*.json'
    options:
      parser: json
  - files:
    - '*.md'
    - '*.markdown'
    options:
      parser: markdown
  - files:
    - '*.ts'
    - '*.tsx'
    options:
      parser: typescript
  - files:
    - '*.yml'
    - '*.yaml'
    options:
      parser: yaml

ESLintの設定を行う

ESLintは静的解析ツールです。コード品質を一定水準に保つのに役立つので追加しましょう。

yarn add --dev --exact eslint
yarn add --dev --exact @typescript-eslint/parser
yarn add --dev --exact @typescript-eslint/eslint-plugin
yarn add --dev --exact eslint-config-prettier
yarn add --dev --exact eslint-plugin-prettier
yarn add --dev --exact eslint-plugin-react
yarn add --dev --exact eslint-plugin-jest
yarn add --dev --exact @react-native-community/eslint-config

.eslintrc.yaml をプロジェクトルートディレクトリ直下に作成します。

---
root: true
extends:
  - plugin:@typescript-eslint/recommended
  - prettier
  - prettier/@typescript-eslint
  - plugin:react/recommended
  - '@reactnativecommunity'
  - plugin:react-native/all
  - plugin:jest/recommended
parser: '@typescript-eslint/parser'
parserOptions:
  project: ./tsconfig.json
  ecmaFeatures:
    jsx: true
plugins:
  - '@typescript-eslint'
  - react
  - react-native
  - jest
settings:
  react:
    version: detect
rules:
  no-console: warn

EditorConfigの設定を行う

EditorConfigの設定を置いておくことで、インデントなどコード形式を統一することができます。
.editorconfig をプロジェクトルートディレクトリ直下に作成します。

root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

実際に雛形アプリを作ってみる

それでは、実際に雛形を作っていきましょう。
ソースコードは全て srcディレクトリ内に含めることとします。
プロジェクトルートディレクトリ直下に App.jsx が配置されていますが、こちらをsrcディレクトリ直下に移動してください。
最終的なディレクトリ構成は以下のようになります。

RNSampleApp/src
├── App.tsx
├── components
│   └── pages
│       ├── Detail
│       │   └── index.tsx
│       ├── Home
│       │   └── index.tsx
│       ├── MyPage
│       │   └── index.tsx
│       ├── Search
│       │   └── index.tsx
│       └── index.ts
├── constants
│   ├── screen.ts
│   └── theme.ts
└── routes
    ├── Main
    │   ├── Home.tsx
    │   └── index.tsx
    └── index.tsx
アプリ必要なパッケージをインストールする

アプリに必要なパッケージを追加でインストールします。
インストール完了後、 package.json を開き、先ほどと同様に ^ のついたパッケージバージョンについては、 ^ を削除しておきましょう。

yarn add @react-navigation/native
yarn add @react-navigation/stack react-native-safe-area-context react-native-gesture-handler react-native-screens @react-native-community/masked-view
yarn add @react-navigation/material-bottom-tabs react-native-paper
yarn add react-native-vector-icons
yarn add @types/react-native-vector-icons

アイコン用のフォントをビルド成果物に含める必要があるので、 ios/RNSampleApp/info.plist に以下の定義を追加してください。

    <key>UIAppFonts</key>
    <array>
      <string>AntDesign.ttf</string>
      <string>Entypo.ttf</string>
      <string>EvilIcons.ttf</string>
      <string>Feather.ttf</string>
      <string>FontAwesome.ttf</string>
      <string>FontAwesome5_Brands.ttf</string>
      <string>FontAwesome5_Regular.ttf</string>
      <string>FontAwesome5_Solid.ttf</string>
      <string>Foundation.ttf</string>
      <string>Ionicons.ttf</string>
      <string>MaterialIcons.ttf</string>
      <string>MaterialCommunityIcons.ttf</string>
      <string>SimpleLineIcons.ttf</string>
      <string>Octicons.ttf</string>
      <string>Zocial.ttf</string>
    </array>

CocoaPodsのリポジトリを更新します。

rm -rf ios/build
cd ios
pod install --repo-update
cd ..
index.jsを修正する
/**
 * @format
 */

import { AppRegistry } from 'react-native';
import App from './src/App'; // './App' から './src/App' に変更する
import { name as appName } from './app.json';

AppRegistry.registerComponent(appName, () => App);

src/constants/screen.tsを作成する

画面名を示す定数を作成します。
今回は、ホーム画面、詳細画面(ホーム画面から遷移)、検索画面、マイページの4画面とします。

export const HOME = 'HOME';
export const MY_PAGE = 'MY_PAGE';
export const DETAIL = 'DETAIL';
export const SEARCH = 'SEARCH';
src/constants/theme.tsを作成する

コンポーネントで使用する色を示す定数を作成します。
今回は、画面下部のタブに使用する2つの色を定義しています。

export const COLOR = {
  BOTTOM_TAB: '#87cefa',
  TAB_ACTIVE: '#000000',
};
各画面を作成する

ここからは4つの画面をそれぞれ作成します。

src/components/pages/Home/index.tsx

ホーム画面を作成します。

import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { DETAIL } from '../../../constants/screen';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default function Home() {
  const { navigate } = useNavigation();
  return (
    <View style={styles.container}>
      <Text>ホーム</Text>
      <TouchableOpacity onPress={() => navigate(DETAIL)}>
        <Text>詳細画面へ遷移する</Text>
      </TouchableOpacity>
    </View>
  );
}
src/components/pages/Detail/index.tsx

詳細画面を作成します。

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

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default function Detail() {
  return (
    <View style={styles.container}>
      <Text>詳細画面</Text>
    </View>
  );
}
src/components/pages/Search/index.tsx

検索画面を作成します。

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

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default function Search() {
  return (
    <View style={styles.container}>
      <Text>検索画面</Text>
    </View>
  );
}
src/components/pages/Mypage/index.tsx

マイページ画面を作成します。

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

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default function MyPage() {
  return (
    <View style={styles.container}>
      <Text>マイページ</Text>
    </View>
  );
}
src/components/pages/index.ts

pages ディレクトリから一括でインポートするためのtsファイルを作成します。

export { default as Detail } from './Detail';
export { default as Home } from './Home';
export { default as MyPage } from './MyPage';
export { default as Search } from './Search';
画面遷移を定義する

各画面を作成した後は、画面遷移を定義します。

src/routes/Main/Home.tsx

ホーム画面と詳細画面の画面遷移を定義します。

import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { DETAIL, HOME } from '../../constants/screen';
import { Detail, Home } from '../../components/pages';

const Stack = createStackNavigator();

function HomeNavigator() {
  return (
    <Stack.Navigator initialRouteName={HOME}>
      <Stack.Screen
        name={HOME}
        component={Home}
        options={{
          title: 'ホーム',
        }}
      />
      <Stack.Screen
        name={DETAIL}
        component={Detail}
        options={{
          title: '詳細画面',
        }}
      />
    </Stack.Navigator>
  );
}

export default HomeNavigator;
src/routes/Main/index.tsx

メイン画面の画面遷移を定義します。

import React from 'react';
import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { HOME, MY_PAGE, SEARCH } from '../../constants/screen';
import { Search, MyPage } from '../../components/pages';
import { COLOR } from '../../constants/theme';
import Home from './Home';

const Tab = createMaterialBottomTabNavigator();

export default function MainNavigator() {
  return (
    <Tab.Navigator
      initialRouteName={HOME}
      activeColor={COLOR.TAB_ACTIVE}
      shifting={true}
    >
      <Tab.Screen
        name={HOME}
        component={Home}
        options={{
          tabBarColor: COLOR.BOTTOM_TAB,
          tabBarLabel: 'ホーム',
          tabBarIcon: ({ color }) => (
            <MaterialCommunityIcons name="home" color={color} size={25} />
          ),
        }}
      />
      <Tab.Screen
        name={SEARCH}
        component={Search}
        options={{
          tabBarColor: COLOR.BOTTOM_TAB,
          tabBarLabel: '検索',
          tabBarIcon: ({ color }) => (
            <MaterialCommunityIcons
              name="map-search-outline"
              color={color}
              size={25}
            />
          ),
        }}
      />
      <Tab.Screen
        name={MY_PAGE}
        component={MyPage}
        options={{
          tabBarColor: COLOR.BOTTOM_TAB,
          tabBarLabel: 'マイページ',
          tabBarIcon: ({ color }) => (
            <MaterialCommunityIcons name="account" color={color} size={25} />
          ),
        }}
      />
    </Tab.Navigator>
  );
}
src/routes/index.tsx

メイン画面の画面遷移定義を NavigationContainer でラップします。

import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import MainNavigator from './Main';

export default function MainRouter() {
  return (
    <NavigationContainer>
      <MainNavigator />
    </NavigationContainer>
  );
}
アプリの起動ポイントとつなぐ

最後に、 src/App.jsx を修正して、作成したアプリが表示されるようにしましょう。

import React from 'react';
import MainRouter from './routes';

export default function App() {
  return <MainRouter />;
}

このようなアプリが表示されれば完成です。

rn-sample-app.gif

おわりに

かなり簡素なひな形ですが、一応 Github に公開しておきました。

やってみた感想

今回は記載を省略しましたが、開発環境構築するだけでも大変だと感じました。
様々なツールの様々な設定をおさえるだけでも結構な時間がかかりそうです。
各コンポーネントやAPIの設定はさらに時間がかかりそうです...。

現在、業務で TeleportというOSSを扱っています。TeleportのUIは、ReactとTypeScriptで構築されています。今までは、UIの実装を見ても何となくしか内容を理解できなかったのですが、今後はもう少しちゃんと理解することができそうです。せっかくの経験なので業務にも活かしていきたいと思います。

今後に向けて

基本的なパッケージしか使用していないので、お手伝いを通してもっと多くの React 、React Native のパッケージに触れたいと思います。推奨されている Expo CLI を使ったひな形も公開したいですね。
また、サーバサイドは AWS Amplify または Firebase を利用する予定です。開発が進んで、さらにノウハウが溜まったらまた何らかの形で整理して共有したいと思います。

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
What you can do with signing up
6