12
1

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 1 year has passed since last update.

qnoteAdvent Calendar 2022

Day 20

ReactNativeでゴルフスコアカウンタアプリ作ってみた(前編)

Posted at

はじめに

今回は、iOSアプリ開発初心者が手探りでReactNativeを使い実際にiOSアプリを開発していく模様をご紹介します。

暖かい目で見守っていただければ幸いです。

対象読者

  • ReactNative初心者
  • 何となく簡単なアプリを作ってみたいと思っている方
  • JavaScript、Reactがある程度扱える方(投稿者は扱えるとは言えない)

この記事でわかること

  • ReactNativeの環境構築 (React Native CLI Quickstart)
  • ReactNativeを使った簡単なゴルフスコアカウントアプリの作り方
  • ある程度動くものが出来上がっていく過程

背景

去年10月、友人の勧めで始めたゴルフ。始めてから1週間でコースに連れて行かれて散々な目にあいましたが、今では予定が合えば休日に友人とショートコース回り放題のゴルフ場へ行き日々修行しています。
(おじさま達が、暇さえあればよく素振りする気持ちがわかるようになりました。)
ゴルフでは、1ホールごとに何打でホールアウトしたのかを記録します。その時に必要なのが、スコアカードです。私は、毎回iPhoneのゴルフスコアアプリで毎回のスコアを記録していました。

その時にふと、思ったのです。

「このアプリってどうやって作られているのか?」
私は普段、業務でPHPを扱っているのでiOSアプリとはどんな感じで開発されているのか
マジでわからなかったので、この良い機会に作ってみようかな…と思ったのが始まりです。

どうやって作るのか?

まず、iOSアプリをどうやって作るのか。からですよね、作ってみたいものの、実際めちゃくちゃ難しそうってのが本音でした…
とりあえず、「iOS アプリ 作り方」で調べてみることに。
そしたら…
実は、難しいと思われがちだが年々開発の難易度は下がってきているみたい。ホントか?笑。
どうやらiOSアプリの開発に適したプログラミング言語としては下記の言語があるみたいです。

  • Swift
  • Java
  • Python
  • Objective-C
  • JavaScript
  • Ruby
  • Dart

そうか、そうか、どれも触れたことがないな…
もう心が折れそうになりました…

ん?JavaScript?一瞬触れたことあるな。一瞬だけ。一瞬だけですよ?

どうやら、JavaScriptを用いたReactNativeというものでiOSアプリを開発できるみたいです。
Reactも一瞬触ったことがある。
iOSアプリ開発に適した言語の中でJavaScriptのみ知ってはいた。が、先ほど話したように、本当に一瞬触っただけだったので開発が不安でしかない…
そこで、身勝手なこととは重々承知ですが、最近一緒にゴルフの練習をした会社の先輩を巻き込み、ペアプログラミングすることにしました。
前編では、ReactNativeとはなんなのかをまとめ、手探りで簡単なゴルフカウンタアプリを作成し、ReactNativeになれるところから入り、後編では先輩とペアでちょっとだけ本格的なゴルフカウンタアプリを作成していきたいと思います。

React Nativeとは

メタ・プラットフォームズ(旧facebook社  )が開発したオープンソースのネイティブアプリケーションフレームワークです。

ネイティブアプリケーションというのはApple Store、GooglePlayからインストールして利用するアプリケーションのことで、みなさんが聞いたことがあるアプリで採用されています!

  • Instagram
  • UberEATS
  • Discord
  • Progate
  • NAVITIME

などなど、人気なアプリに採用されていることもあってなんか安心感がありますね。
他にも、メリットデメリットについてもまとめてみました。

ReactNativeのメリット

  • 開発が効率的
    • クロスプラットフォームのアプリ開発用フレームワークであり、iOSとAndroidのアプリを1つの言語で同時に開発できる
  • 瞬時に検証できる
    • ホットリロード機能があるので、瞬時にソースコードを反映することができる
  • Reactを使ったことのある人には学習が簡単
    • Reactを使用してWebアプリケーション開発を行っている人には、学習コストが少ない

ReactNativeのデメリット

  • 大規模アップデートが頻繁に行われる
    • ReactNativeは頻繁にアップデートが行われるため、アップデートの度に環境も合わせてアップデートしなければいけない。アプデートが要因で、今まで動いていたアプリが動かなくなってしまったり、予期せぬエラーを招いたりしてしまう。
  • エラーの原因が追いにくい
    • エラーが発生した場合、ReactNativeではそのエラーが「JavaScriptレイヤーで起きているエラーなのか」「ネイティブレイヤーで起きているエラーなのか」が判断しづらい傾向にあります。
  • ネイティブエンジニアには学習コストがかかる
    • ReactNativeは普段からネイティブ開発を行っているエンジニアは、
    • ReactNativeはJavaScriptで記述されるので、普段からSwift、Javaなど、ネイティブ開発を行っているエンジニアの場合、1からJavaScriptを習得する必要があります。

メリットデメリットをまとめると、普段からJavaScript、Reactを扱っているフロントエンジニアの方にとっては開発に取っ掛かりやすく、開発が効率的であることがわかりました。

それに対して、ネイティブエンジニアにとっては学習コストが高めで、頻繁に大規模アップデートが行われることから、アプリの運用がめんどいことがわかりました。

環境構築

全体の流れとしては、以下の通りです。

  • Homebrewのインストール

    • Macのパッケージ管理ツール
  • Node.jsのインストールが必須

    • JavaScript実行環境
  • Watchmanのインストール

    • React Nativeではファイルの変更時に自働で再ビルドを行うが、そのファイル検知のためにWatchmanを利用する
  • rbenvのインストール

  • Xcodeをインストール

  • CocoaPodsをインストール

ReactNativeの公式ドキュメントを参考に上から順に進めていきます。

参考 : 

まずは、Expo Go Quickstartか、ReactNative CLI Quickstartかを選びます。

  • モバイル開発が初めての場合はExpoGo
  • モバイル開発に精通している場合はReactNativeCLI

モバイル開発に精通していない初心者ですが、思い切ってReactNativeCLIを選びました。理由は特にないです。
スクリーンショット 2022-11-27 21.39.08.png
次にOSを選びます。
今回はmacでiOSアプリを作りたいので、macOSとiOSを選びました。
スクリーンショット 2022-11-27 21.46.33.png
次に依存関係のインストールです。

  • Homebrewのインストール
    • Macのパッケージ管理ツール

Homebrewをインストールしていない方はインストールしておきましょう。

  • Node.jsのインストール
    • JavaScript実行環境
  • Watchmanのインストール
    • React Nativeではファイルの変更時に自働で再ビルドを行うが、そのファイル検知のためにWatchmanを利用する

JavaScriptの実行環境が必要なので、Node.jsをインストールします。また、ReactNativeでは、ファイルの変更時に自動で再ビルトが行われ、そのファイルの変更の検知をするためにWatchmanをインストールします。
スクリーンショット 2022-11-27 21.58.47.png

  • rbenvのインストール

ReactNativeは、.ruby-versionファイルを使用して、Rubyのバージョンが必要なものと一致していることを確認するそうです。
また、この後インストールするCocoapodsにRubyが必要だそうです。
指示に従ってRubyのバージョンマネージャーをインストールし、適切なRubyのバージョンをインストールしました。

何をしたか、一部始終を記録しておきます。
公式の手順に乗っ取り初期化

rbenv init

.zshrcに下記を追記しろと言われるので、追記する。

eval "$(rbenv init -)"

rbenv-doctorをたたいて、問題ないことを確認する。

curl -fsSL https://github.com/rbenv/rbenv-installer/raw/main/bin/rbenv-doctor | bash

インストール出来るRubyのバージョン一覧を確認。

rbenv install -l

ここでどん詰まりしました。どうやら2.7.5でいけるっぽいのでインストールする。

rbenv install 2.7.5

2.7.5を設定する

rbenv global 2.7.5

Rubygemsをインストールする

gem install bundler

ターミナルを再起動してRubyのバージョン確認

ruby -v
ruby 2.7.5p203
  • Cocoapodsをインストール
brew install cocoapods
  • Xcodeをインストール
    Xcodeをインストールしましょう。(インストールは気持ち長めです)
    最も簡単な方法としては、macのAppStore経由でインストールするよいと公式では仰ってます。

ここまできたら、プロジェクトを作成しましょう!
プロジェクトを配置したい場所に下記を実行します。
AwesomeProjectの箇所には、任意のプロジェクト名を入れます。

npx react-native init AwesomeProject

そうするとこのように表示されます!!
スクリーンショット 2022-11-27 23.22.05.png

次にプロジェクトフォルダー内で次のコマンドを実行します。

npx react-native run-ios

そうすると、シュミレータが起動し準備が整います。
スクリーンショット 2022-11-27 23.33.36.png

カウントアプリを作ってみる

実際にゴルフカウンターアプリというものを作っていきましょう!
手探りでなんとなく作っていきます。

仕様

まず、ゴルフカウンターとはゴルフをやる上で必要不可欠なものです。
自分が何回打ったのかを忘れてしまわないように記録しておくための便利なアイテムです。
参考 :

機能

今回は、簡単に下記の機能を実装していきたいと思います。

  • 現在のホールの記録
  • 現在のホールのパット数
  • 現在の打数

では、実装していきましょう!

ソースコード

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * @format
 * @flow strict-local
 */

import React, {useState} from 'react';

import type {Node} from 'react';

import {
  StyleSheet,
  Text,
  View,
  TouchableOpacity,
  FlatList,
  Button,
} from 'react-native';

const App: () => Node = () => {
  const [number, setNumber] = useState(0)
  const [score, setScore] = useState([])
  const [holeCount, setHoleCount] = useState(1)
  const [parCount, setParCount] = useState(3)

  const increase = () => {
    if (number < 20) {
      setNumber(number + 1)
    } 
  }

  const decrease = () => {
    if (number > 0) {
      setNumber(number - 1)
    }
  }

  const clear = () => {
    setNumber(0)
  }

  const holeIncrease = () => {
    if (holeCount < 18) {
      setHoleCount(holeCount + 1)
    }
  }
  const holeDecrease = () => {
    if (holeCount > 1) {
      setHoleCount(holeCount - 1)
    }
  }
  const parIncrease = () => {
    if (parCount < 5) {
      setParCount(parCount + 1)
    }
  }
  const parDecrease = () => {
    if (parCount > 3) {
      setParCount(parCount - 1)
    }
  }

  const onRegistration = () => {
    const newScore = [...score, {
      hole: holeCount,
      par: parCount,
      score: number,
      key: Math.random().toString(),
    }]
    setNumber(0)
    setScore(newScore)
  }

  // Score情報を返す
  const renderItem = ({item}) => {
    return (
      <View style={styles.scoreItem}>
        <View>
          <Text style={styles.scoreItemText}>H{item.hole}</Text>
        </View>
        <View>
          <Text style={styles.scoreItemText}>Par{item.par}</Text>
        </View>
        <View>
          <Text style={styles.scoreItemText}>Score {item.score}</Text>
        </View>
      </View>
      )
  };

  return (
    <View style={styles.container}>
        {/* ホール情報選択 -Start- */}
        <View style={styles.basicPreference}>
          <View>
            <TouchableOpacity onPress={holeDecrease}>
              <View style={styles.holeSettingButton}>
                <Text></Text>
              </View>
            </TouchableOpacity>
          </View>
          <View style={styles.ViewBasicPreference}>
            <Text style={styles.ViewBasicPreferenceText}>
              H{holeCount}
            </Text>
          </View>
          <View>
            <TouchableOpacity onPress={holeIncrease}>
              <View style={styles.holeSettingButton}>
                <Text></Text>
              </View>
            </TouchableOpacity>
          </View>
          <View>
            <TouchableOpacity onPress={parDecrease}>
              <View style={styles.holeSettingButton}>
                <Text></Text>
              </View>
            </TouchableOpacity>
          </View>
          <View style={styles.ViewBasicPreference}>
            <Text style={styles.ViewBasicPreferenceText}>
              Par{parCount}
            </Text>
          </View>
          <View>
            <TouchableOpacity onPress={parIncrease}>
                <View style={styles.holeSettingButton}>
                  <Text></Text>
                </View>
            </TouchableOpacity>
          </View>
        </View>
        {/* ホール情報選択 -End- */}

        {/* Scoreの結果表示 -Start- */}
        <View style={styles.scoreContainar}>
          <View style={styles.viewScore}>
            <FlatList
              data={score}
              renderItem={renderItem}
            />
          </View>
        </View>
        {/* Scoreの結果表示 -End- */}

        {/* カウンタ表示 -Start- */}
        <View style={styles.viewCounterContainer}>
          <View style={styles.viewCounter}>
            <Text style={styles.viewCounterText}>{number}</Text>
          </View>
        </View>
        {/* カウンタ表示 -End- */}

        {/* カウンタボタン -Start- */}
        <View style={styles.countButtonContainer}>
          <TouchableOpacity onPress={decrease}>
            <View style={styles.decreaseButton}>
              <Text style={styles.countButtonText}>-</Text>
            </View>
          </TouchableOpacity>
          <TouchableOpacity onPress={increase} >
            <View style={styles.IncreaseButton}>
              <Text style={styles.countButtonText}>+</Text>
            </View>
          </TouchableOpacity>
        </View>
        {/* カウンタボタン -End- */}

        {/* Crearボタン、決定ボタン -Start- */}
        <View style={styles.buttonItemContainer}>
          <TouchableOpacity style={styles.clearButton}>
          <Button
            title='Clear'
            onPress={clear}
            color='black'
            />
          </TouchableOpacity>
          <TouchableOpacity style={styles.registrationButton}>
            <View>
              <Button
              title='OK!'
              onPress={onRegistration}
              color='black'
              />
            </View>
          </TouchableOpacity>
        </View>
        {/* Crearボタン、決定ボタン -End- */}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    backgroundColor: 'black',
  },

  basicPreference: {
    flexDirection: 'row',
    width: '100%',
    justifyContent: 'center',
    marginTop: 90,
  },

  holeSettingButton: {
    height: 50,
    width: 50,
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 10,
    borderWidth: 2, 
    backgroundColor: 'white',
  },
  ViewBasicPreference: {
    height: 50,
    width: 80,
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 10,
    borderColor: 'white',
    borderWidth: 1,
  },
  ViewBasicPreferenceText: {
    color: 'white',
    fontWeight: 'bold',
    fontSize:30,
  },

  scoreContainar: {
    height: '30%',
    width: '100%',
    alignItems: 'center',
    marginTop: 10,
  },
  viewScore: {
    height: '100%',
    // borderWidth: 1,
    // borderColor: 'white',
    alignItems: 'center',
  },

  scoreItem: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    height: 50,
    width: 300,
    backgroundColor: 'gainsboro',
    marginBottom: 3,
    padding: 10,
  },
  scoreItemText: {
    fontSize:20,
    fontWeight: 'bold',
  },

  viewCounterContainer: {
    width: '100%',
    alignItems: 'center',
    justifyContent: 'center',
    marginTop: 70,
  },
  viewCounter: {
    width: '20%',
    alignItems: 'center',
    backgroundColor: 'black',
    borderRadius: 10,
    borderWidth: 1,
    borderColor: 'white',
  },
  viewCounterText: {
    fontSize: 50,
    fontWeight: 'bold',
    color: 'white',
  },

  countButtonContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-evenly',
    width: '100%',
    backgroundColor: 'black',
    marginTop: 20,
    marginBottom: 20,
  },
  IncreaseButton: {
    width: 100,
    height: 100,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: 'red',
    borderRadius: 50,
  },
  decreaseButton: {
    width: 100,
    height: 100,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: 'blue',
    borderRadius: 50, 
  },
  countButtonText: {
    color: 'white',
    fontSize: 60,
    fontWeight: 'bold',
  },

  buttonItemContainer: {
    width: '100%',
    flexDirection: 'row',
    justifyContent: 'space-evenly',
    marginTop: 10,
  },

  clearButton: {
    backgroundColor: 'white',
    height: 50,
    width: 100,
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 10,
    borderWidth: 2,
    borderColor: 'gray',
  },
  registrationButton: {
    backgroundColor: 'white',
    height: 50,
    width: 100,
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 10,
    borderWidth: 2,
  },
});

export default App;


結果

golfcounter_gif.gif

解説

アプリの画面については下記の構成です。
① ホール情報の選択
② スコアの表示
③ カウンタ
④ ClearボタンとOKボタン

図1.png

① ホール情報の選択

ホール情報の状態を管理するために、useStateを宣言します。
(React若干触っててよかった...)

  const [number, setNumber] = useState(0)
  const [score, setScore] = useState([])
  const [holeCount, setHoleCount] = useState(1)
  const [parCount, setParCount] = useState(3)
        {/* ホール情報選択 -Start- */}
        <View style={styles.basicPreference}>
          <View>
            <TouchableOpacity onPress={holeDecrease}>
              <View style={styles.holeSettingButton}>
                <Text></Text>
              </View>
            </TouchableOpacity>
          </View>
          <View style={styles.ViewBasicPreference}>
            <Text style={styles.ViewBasicPreferenceText}>
              H{holeCount}
            </Text>
          </View>
          <View>
            <TouchableOpacity onPress={holeIncrease}>
              <View style={styles.holeSettingButton}>
                <Text></Text>
              </View>
            </TouchableOpacity>
          </View>
          <View>
            <TouchableOpacity onPress={parDecrease}>
              <View style={styles.holeSettingButton}>
                <Text></Text>
              </View>
            </TouchableOpacity>
          </View>
          <View style={styles.ViewBasicPreference}>
            <Text style={styles.ViewBasicPreferenceText}>
              Par{parCount}
            </Text>
          </View>
          <View>
            <TouchableOpacity onPress={parIncrease}>
                <View style={styles.holeSettingButton}>
                  <Text></Text>
                </View>
            </TouchableOpacity>
          </View>
        </View>
        {/* ホール情報選択 -End- */}

ReactNativeではHTMLタグが使えないので、ReactNativeが用意してくださったcomponentを使用します。HTMLに例えるとViewはdivタグのようなものです。
TouchableOpacityはbuttonと似ており、タップすると透明になります。
このTouchableOpacityのonPressメソッドが実行された時にそれぞれの関数が実行され、状態が更新されます。
「>」ボタンが押されたら+1。「<」ボタンが押されたら-1。という感じです。
parCountの初期値が3というのは、一般的にパット数は3,4,5ですので初期値は3にしています。

  const holeIncrease = () => {
    if (holeCount < 18) {
      setHoleCount(holeCount + 1)
    }
  }
  const holeDecrease = () => {
    if (holeCount > 1) {
      setHoleCount(holeCount - 1)
    }
  }
  const parIncrease = () => {
    if (parCount < 5) {
      setParCount(parCount + 1)
    }
  }
  const parDecrease = () => {
    if (parCount > 3) {
      setParCount(parCount - 1)
    }
  }

② スコアの表示

        {/* Scoreの結果表示 -Start- */}
        <View style={styles.scoreContainar}>
          <View style={styles.viewScore}>
            <FlatList
              data={score}
              renderItem={renderItem}
            />
          </View>
        </View>
        {/* Scoreの結果表示 -End- */}

こちらが、スコアの表示部分です。
FlatListを使うことによってリストを表示させることができます。
順番的には、ホール選択、パット数選択、打数入力終了後にOKボタンが押された時にスコアを表示するため、

const [score, setScore] = useState([])

実際には、こちらにスコア情報が表示される頃にはスコア情報のオブジェクトが追加されている状態です。
そこで、dataにオブジェクトが代入された配列を代入、renderItemにスコア情報を返す関数を代入します。

<FlatList
    data={score}
    renderItem={renderItem}
/>
  // Score情報を返す
  const renderItem = ({item}) => {
    return (
      <View style={styles.scoreItem}>
        <View>
          <Text style={styles.scoreItemText}>H{item.hole}</Text>
        </View>
        <View>
          <Text style={styles.scoreItemText}>Par{item.par}</Text>
        </View>
        <View>
          <Text style={styles.scoreItemText}>Score {item.score}</Text>
        </View>
      </View>
      )
  };

③ カウンタ

        {/* カウンタ表示 -Start- */}
        <View style={styles.viewCounterContainer}>
          <View style={styles.viewCounter}>
            <Text style={styles.viewCounterText}>{number}</Text>
          </View>
        </View>
        {/* カウンタ表示 -End- */}

        {/* カウンタボタン -Start- */}
        <View style={styles.countButtonContainer}>
          <TouchableOpacity onPress={decrease}>
            <View style={styles.decreaseButton}>
              <Text style={styles.countButtonText}>-</Text>
            </View>
          </TouchableOpacity>
          <TouchableOpacity onPress={increase} >
            <View style={styles.IncreaseButton}>
              <Text style={styles.countButtonText}>+</Text>
            </View>
          </TouchableOpacity>
        </View>
        {/* カウンタボタン -End- */}

カウンタ表示部分では、現在の打数が表示されます。
裏側の処理としては、ホール情報選択の時と一緒です。
ボタンが押され次第状態が更新されていきます。
「+」ボタンが押されれば+1「-」ボタンが押されれば-1という感じです。

  const increase = () => {
    if (number < 20) {
      setNumber(number + 1)
    } 
  }

  const decrease = () => {
    if (number > 0) {
      setNumber(number - 1)
    }
  }

④ ClearボタンとOKボタン

{/* Clearボタン、決定ボタン -Start- */}
        <View style={styles.buttonItemContainer}>
          <TouchableOpacity style={styles.clearButton}>
          <Button
            title='Clear'
            onPress={clear}
            color='black'
            />
          </TouchableOpacity>
          <TouchableOpacity style={styles.registrationButton}>
            <View>
              <Button
              title='OK!'
              onPress={onRegistration}
              color='black'
              />
            </View>
          </TouchableOpacity>
        </View>
        {/* Clearボタン、決定ボタン -End- */}

Clearボタンが押された時にカウンタの状態を0にしています。
OKボタンが押された時は、scoreの配列にスコア情報のオブジェクトを追加してスコアの結果を表示します。

  const clear = () => {
    setNumber(0)
  }

  const onRegistration = () => {
    const newScore = [...score, {
      hole: holeCount,
      par: parCount,
      score: number,
      key: Math.random().toString(),
    }]
    setNumber(0)
    setScore(newScore)
  }

感想

JacaScript、React、HTML、CSSなどのWeb技術がある程度わかっている、かつ、公式ドキュメント、ReactNativeについての記事を読み漁ればかろうじて動くアプリが作れることがわかりました。ちょっと、環境構築怪しいけど。笑
気合いでなんとかなりました...

本年は大変お世話になりました。どうぞ良いお年をお迎えください。

12
1
0

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
12
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?