LoginSignup
260
205

More than 1 year has passed since last update.

筋肉にちゃんと「お願いマッスル!」するために、1からReact学んでアプリをリリースした話

Last updated at Posted at 2021-01-01

2020年の秋に、個人的にReact(expo)でアプリを制作・リリースしました。
日頃の業務はHTMLのマークアップ・コーディング・JSはやってもバニラかjQueryなので、案件ではないとはいえ、初めてやってみてコンポーネントの概念など勉強になりました。
主に参考記事の抜粋が多いですが、アプリを作った中でつまづいたこと・学んだことを書きたいと思います。

作ったアプリについて

pitch-mo-voice-counter.png
ピッチモボイスカウンター

※2021年末にこのアプリは非公開にいたしました。お試しいただいた方、ありがとうございました!

「1、2、3、1回」、「1、2、3、2回」...のようにピッチ(1、2、3のリズム)と回数の両方を声でカウントするアプリです。
pitch-mo-voice-counter-phone.png

広告は一切入れておらず、完全無料です。
(お金儲けするつもりはなく、自分が筋トレできればいいし、広告を入れるとユーザー(私)の使う気が50%減るため)

全体的なソースコードはこちら→ https://github.com/sararilfy/pitch-mo-voice-counter/blob/develop/App.js

なぜアプリを作ったのか

まず会社の健康診断で悪玉コレステロールの値が要精密だったという背景があり、どうにかして健康に戻したいと考えていました。そんな中、テレビの「金曜日のス●イルたちへ」という番組でヒットしている筋トレ本を知りました。
はじめてのやせ筋トレ
やせ筋トレ 姿勢リセット(第2巻)

この本で筋トレを習慣にしようとしました。
しかし、究極に体が硬い私は体を動かすので精一杯で、何秒経ったかなんて数える余裕がないのです。
そして体を動かすスピードが速すぎて筋肉に効いてなかったり、何回やったか忘れて途中で諦めてしまったり。

それを解決するべくボイスカウンターアプリを探しました(アプリをいちいち見るのも面倒だったためボイスアプリ)。
しかし、声でピッチ(ペース)の秒数と回数の両方教えてくれるアプリはありませんでした(どちらか片方対応、というのはあったんですけどね...)。

自分で作るか...と静的HTMLで音声読み込んで音が出るようにJSでプログラムを書いていたところに、プログラマーの夫が「これはアプリにしちゃえば?」と言われたので、アプリ化することにしました。

Reactの基礎は、React公式のチュートリアルのみで学んだ

取っ掛かりはReact Native+Expoではじめるスマホアプリ開発という本を参考にしました。
ただ、この本はExpoの概要的な本で、Reactの基礎は載っていなかっため、Reactの本を探しました。
が、本があまりない!(あっても載っている情報がだいぶ古いバージョンのもの)

...ということで、React公式のチュートリアルをやることにしました。
これがめっちゃわかりやすかった!!!なんて親切なドキュメントなんだろうと思いました。
チュートリアルを通したあと、ドキュメントを1〜12までしっかり読み進めて、これだけでReactの概要は掴むことができました。

さらに、作り方を導いてくれるReactの流儀ページがあり、本当にこの通りの手順で形にしていきました。
最終的に、本は買わずに済みました。

アプリデザインについて

Reactの流儀 によると、まずはモックアップが必要とのことだったので、デザインをつくることにしました。

▼デザインソフトは、慣れているAdobe XDで作成

アプリデザインカンプ

自分の理想を詰め込んだデザイン。

  • 数字の入力は親指1本だけにしたいので、ピッカー。
  • あとどのくらいで終わるかひと目でわかりたいので、円がアニメーションする形
  • ボタンは押しやすいように下に配置

デザインの参考にDribbbleピンタレストで「workout app」とかで検索して見まくりました。

以下のリンクの「UIキット」も、アプリデザインの参考にしました。(余白の取り方、どう境目を作るかなど。)
https://www.adobe.com/jp/products/xd/resources.html

あとは、Disabledボタンは薄くするといいとのことで、これも採用しました。
UXデザイナーから学ぶ、無効状態のボタンをグレーアウトにしない理由 | コリス

また、コンポーネントを考えるにあたって、Atomic Designの記事を読み、単純にグルーピングするのではなく、原子レベルで小さく考える...ところとか参考にしました。

Reactの流儀をもとにしたアプリ構築の流れ

Expoの環境構築の後、このチュートリアルの手順でアプリを作りました。

Step 1: UIをコンポーネントの階層構造に落とし込む

コンポーネント設計のノート

こう見返すと汚い(汗)
ノートに手書きで、ひたすらに原子レベルで名前をつけていきました。
初心者だからか、原子レベルを気にしすぎてコンポーネントを細かくしすぎました。
するとスタイル(デザイン)が調整しずらくなり、作りながらコンポーネントの断舎離(?)を繰り返していくハメ(?)になりました。

Step 2: Reactで静的なバージョンを作成する

一切 state を使わないでくださいと書いてあったので、言われたとおりにpropsだけ使って構築しました。
ここまでで全く動かない見た目だけ綺麗なアプリができました。

Step 3: UI 状態を表現する必要かつ十分な state を決定する

  1. 親から props を通じて与えられたデータでしょうか? もしそうなら、それは state ではありません
  2. 時間経過で変化しないままでいるデータでしょうか? もしそうなら、それは state ではありません
  3. コンポーネント内にある他の props や state を使って算出可能なデータでしょうか? もしそうなら、それは state ではありません

この3つの質問を投げかけながら、これまで作ってきたどのPropsをStateに変更すべきなのか、考えました。

最初は数字の部分は全部Stateだと思っていました。(毎秒数が変わるから)
しかし、毎秒カウントなどの任意のタイミングで数値を変更するという仕組みにしないといけない場合、親コンポーネントにstate置いて、残りはpropsでデータを下に渡していくべきだと気づいて、次のStep4で結構何度も設計し直しました。

Step 4: state をどこに配置するべきなのかを明確にする

何度も何度も何度も作り変えて試行錯誤した結果、最終的にこのようなStateとpropsの構成になりました(一部)。
親コンポーネントのWorkoutVoiceCounterにStateをぜーんぶ持たせて、親コンポーネントの関数でStateの値を変更したあと、子コンポーネントにPropsで値を渡します。

Stateとpropsの構成

Stateとpropsの構成1

デザイン上はこんな感じ

Stateとpropsの構成

振り返って見るとネーミングのばらつきが気になります...。

Step 5: 逆方向のデータフローを追加する

今度の壁は、子や孫コンポーネントで変更があった場合に、どうやって親コンポーネントを変更するか?

難所だったのがコンポーネント構造の都合上、子コンポーネントだけでなく孫コンポーネントができてしまい、孫コンポーネントまでデータを渡したり、孫コンポーネントのボタンをタップしたら親コンポーネントのStateを変えたりしないといけなくなったことです。
苦労を重ねた結果、「バケツリレー」という結論にいたりました。

まず、子が何かしたときに親のStateにアクセスするためには、親に関数定義して、子に関数を渡す必要があるとのこと。
さらに子に関数を渡すときはバインドが必要と以下ページから学びました。
ReactのDocs:コンポーネントに関数を渡す

今度は、親から孫コンポーネントに一気に関数を渡そうとしましたが、できず、めちゃめちゃググった結果、子にPropsを通した後に、子から孫へPropsで渡すというバケツリレーしてもらうしかないとたどり着きました。しかも関数の形で渡すんですね。
(以下のQiita書いてくれた人に感謝!)

Qiita: [Reactコンポーネント]孫から親のイベントを呼びたい時
記事がNot Foundになってしまいました。。。

関数は届いたけど、引数が届かないなんてこともあり、引数の枠も子へ孫へバケツリレーしないといけない!...こともありました。

最初は各コンポーネントごと計算する関数を入れ込んでいたのですが、最終的に全部親(祖父?)コンポーネントで計算処理して、結果データを子や孫に落としていく感じになりました。

以上、「Reactの流儀」はここまでで、あとは欲しい機能をひたすら追加していくのみでした。

React (expo)でハマった問題と対処集

完成するまでに何度も分からなかったり、何度もエラーにハマったり、壁にぶち当たったりました。
その時どう解決したかを記載していきます。

今回大きく学んだこと1 「チュートリアルは必ず最初に触って、Docsにも目を通しておこう」

今思えばエンジニアとしてあるまじき行為...。

私は最初、expoの本を借りて、expoの導入の仕方を習ったのと、「Stateをあれこれするんだよ」っていう口頭のアドバイスのみでReactに取り組んでしまい、基本を理解せずに本やネットのソースコードをコピペして「動いたー!」...という進め方をやってしまいました。

ある程度までは形になったものの(よく行けたな...)、コンポーネントは肥大化、renderって何?どこに処理書いたらいいんやろ?…。案の定、途中で完全に行き詰まってしまい、「Reactをゼロから学んで作り直そう...」と1からやり直す羽目になりました。

その時、企画からすでに半年たっていましたが、Reactをきちんと学んでからは一気に3ヶ月で完成に至りました。
学び直す前は同じ記述のソースコードが何回も何回も並んでいましたが、それも無くなってスッキリ読みやすくなりました。
Reactが分かっていくたびにアプリづくりも楽しくなりました(学生で嫌いな教科がわかり始めて好きになる感じ)。

すごく当たり前なのですが、新しいものを触る時はドキュメント読んで、チュートリアルくらいは触ろう!...と過去の自分に言いたいです。
React甘く見過ぎでした。

今回大きく学んだこと2 「Stack Overflow は頼りになる。英語で検索」

アプリ制作中何度もエラーに遭遇して、すごく助けてくれたのがStack Overflowでした。
そして英語で検索したほうが、ほしい回答にたどり着けました。英会話に通っていたおかげで、抵抗なく英語で検索できました(通っていて良かった!)。

例) 「XXXが動かない」 → 「XXX is not working」

英単語をつなげるだけでも、検索結果にでてきてくれました。
このコツを習得しただけでも(私の中で)大きい気がします。
ただ、読むときはまだGoogle翻訳だよりです(汗)

Pickerが動かない

悩んだランキング第3位。
RactNativeでPickerを利用する
を読みながら、Pickerを実装できるところまでは行きました。
しかし、Pickerは合計5個並ぶので、Pickerを5回書くのではなく、Pickerコンポーネント1個だけにして、配列でそれぞれ別の値をもたせてテンプレートのように使いまわしたかったのです。

あれこれ変更してみても動かない...試行錯誤して、以下の「繰り返しはtoString()が必要だった」という結論にたどり着きました。
React Native で Pickerのitemをmap()で簡潔に実装する方法

<Picker.Item
 key={i}
 label={i.toString()}
 value={i}
/>

ボタンを押したときに、背景色を変更したい

親コンポーネントに「backgroundColor」というStateを準備すれば可能でした

<View style={[styles.background, {backgroundColor: this.state.backgroundColor}]}>
</View>

参考: React Native Dynamically Change View’s Background Color

ダークモード対応

参考1: Appearance - Expo Documentation
参考2: 【React Native】Expo SDK 35で大雑把にダークモードに対応した時のメモ

ダークモードに切り替えたときに瞬時にチェンジするのは、componentDidMountの中に関数を書いてできました!
ただ、色を全部State設定にする必要があったのがちょっと面倒でした。
以下、ソースコードの抜粋。

const
    THEME_COLORS = {
        light: {
            textColor : "#333333",
            backgroundColorSetting : "#f1f0f2",
            backgroundColorCount : "#ffffff",
            backgroundColorPickerCard : "#ffffff",
            colorMain : "#f87c54",
            colorInterval : "#4ac08d",
            colorBackgroundCircle : "#e6e6e6",
            statusBarColor : "dark-content"
        },
        dark: {
            textColor : "#dddddd",
            backgroundColorSetting : "#1a2744",
            backgroundColorCount : "#1a2744",
            backgroundColorPickerCard : "#424e69",
            colorMain : "#fa8f6d",
            colorInterval : "#72C19f",
            colorBackgroundCircle : "#424e69",
            statusBarColor : "light"
        },
    };

class WorkoutVoiceCounter extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            backgroundColor: THEME_COLORS[colorSchemeName].backgroundColorSetting,
            mainKeyColor: THEME_COLORS[colorSchemeName].colorMain,
            textColor: THEME_COLORS[colorSchemeName].textColor,
            backgroundColorSetting: THEME_COLORS[colorSchemeName].backgroundColorSetting,
            backgroundColorCount: THEME_COLORS[colorSchemeName].backgroundColorCount,
            backgroundColorPickerCard: THEME_COLORS[colorSchemeName].backgroundColorPickerCard,
            colorMain: THEME_COLORS[colorSchemeName].colorMain,
            colorInterval: THEME_COLORS[colorSchemeName].colorInterval,
            colorBackgroundCircle: THEME_COLORS[colorSchemeName].colorBackgroundCircle,
            statusBarColor: THEME_COLORS[colorSchemeName].statusBarColor
        };
    }

    _setModeColor = (colorScheme) => {
        this.setState({
            textColor: THEME_COLORS[colorScheme].textColor,
            backgroundColor: THEME_COLORS[colorScheme].backgroundColorSetting,
            backgroundColorSetting: THEME_COLORS[colorScheme].backgroundColorSetting,
            backgroundColorCount: THEME_COLORS[colorScheme].backgroundColorCount,
            backgroundColorPickerCard: THEME_COLORS[colorScheme].backgroundColorPickerCard,
            colorMain: THEME_COLORS[colorScheme].colorMain,
            colorInterval: THEME_COLORS[colorScheme].colorInterval,
            colorBackgroundCircle: THEME_COLORS[colorScheme].colorBackgroundCircle,
            statusBarColor: THEME_COLORS[colorScheme].statusBarColor
        });
    }

    async componentDidMount() {
        Appearance.addChangeListener(({ colorScheme }) => {
            colorSchemeName = colorScheme;
            this._setModeColor(colorScheme);
        });
    }
}

カウントが走っている中にダークモードに変更すると、以下エラーが出てしまうので、カウント中はSetStateしないようにif文でフラグを用意したり、最初の設定に来たら必ず今ダークモードなのかチェックを走らせるように工夫しました。

Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in %s.%s, the componentWillUnmount method,

音声の問題

悩んだランキング第2位。
そもそも、Expoで音をどう出すのかが分からない。
ずーーーーっとググってて、やっとこの記事にたどり着いて、真似したらやっと音出たー!
(ここからやっと1歩すすんだのです、感謝!!!)
【ReactNative】【Expo】画面遷移のタイミングで音を鳴らす方法

最終的に、以下ソースコードを参考にしてやっと出来上がりました。
expo/playlist-example

同じ音を流すと2回目からエラーで流れない

AV - Expo Documentation
Audio - Expo Documentation
上記のページを何度も読み込みました。なんかよくわからなかったのですが、.unloadAsync()と.replayAsync(0)組み合わせるなど試行錯誤した結果、同じ音声だったとしても、音声を出すたび毎回ロードすることになりました。

たくさん音源があるので、スッキリまとめて記述したい

「いち」「にー」「さん」...「じゅっかい」など数を言わないといけないので、音声ファイルが29ファイルありました。
変数とかにいれて、いい感じにスマートに書きたかったのですが、
react-nativeでiOSアプリを作ってみた時のエラーあれやこれ

上記記事にRequireは変数にできないと書いてある…。

また、How to get absolute path of a file located in the assets folder with react-native and expo?を参考に、Asset - Expo Documentationを導入していたのですが、音が遅れる問題が発生したので、Assetをアンインストールして、createAsyncじゃなくて、loadAsyncで相対パス指定。記述は増えたが、これじゃないと途切れてしまう。。。(今毎秒ごとにhttp読み込んでるため?)
スマートに書くのは諦め、地道に書くことに。
以下ソースコードの抜粋。


const soundPath = "./assets/sounds/";

_loadSound = async (isNumber, soundLabel) => {
    if (this.playbackInstance != null) {
        await this.playbackInstance.unloadAsync();
    } else {
        this.playbackInstance = new Audio.Sound();
    }
    switch (soundLabel) {
        case 1:
            await this.playbackInstance.loadAsync(require(soundPath + "info-girl1_info-girl1-ichi1.mp3"));
            break;
        case 2:
            await this.playbackInstance.loadAsync(require(soundPath + "info-girl1_info-girl1-ni1.mp3"));
            break;
        case 3:
            await this.playbackInstance.loadAsync(require(soundPath + "info-girl1_info-girl1-san1.mp3"));
            break;

     // 以下同じ記述が26個つづく

    }
}

円アニメーションどうやったらできるんだ問題

円

悩んだランキング第1位。

ひとまず、SVGで静的な円を書くところまではreact-native-svgを導入していけました。
参考1: Svg - Expo Documentation
参考2: Github react-native-svg/react-native-svg #circle
参考3: 10分でわかるSVG 基礎編

ここからが問題。どうやってアニメーションさせるんだ...?
ここ何日も何日も悩んでいました。

Youtubeに海外の人がReactのコーディング動画をあげていて、参考にしようと見るとsign・cosign・tangent...。高校生で習ったけど忘れた...分からない。

出口が見えないまま、ある日ふと以下記事を見る。
SVGで円をアニメーションさせたい時のMEMO

stroke-dasharray...?......( ゚д゚)ハッ!

react-native-svgstroke-dasharrayは......表現できることを確認!
stroke-dasharrayをStateで変化させていけば良いのでは......とやってみたところ、
うおあー!できたあああああCircleのアニメーション!!!!

上の記事に感謝!!!
Stateすればことに何で今まで気がつかなかったんだー!
悩んだ割には、やり方は単純でした。

iPhone実機で表示確認するには?

表示確認はExpo Clientアプリで行いました。
PCのシュミレーターではいい感じでも、iPhoneで見てみると、ボタンの文字が切れていたり、文字によって表示にガタつきがあったりしました。やはり実機で見ないといけないですね。

スプラッシュスクリーン作って読み込んでみた

スプラッシュスクリーンはアプリのLoading画面にも使えたので、読込中は背景色もオレンジに変更しました。
画像は resizeModecover に設定して余白が出ないようにしました。
参考1: Create a Splash Screen - Expo Documentation
参考2: SplashScreen - Expo Documentation

app.json
{
  "expo": {
    "name": "ピッチモボイス",

    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "cover",
      "backgroundColor": "#f87c54"
    }
  }
}

エラーが起きた時にログをメールで飛ばしたい

Bugsnagというものを見つけました。
https://www.bugsnag.com/

Expoのドキュメントにも導入の仕方が載っていました。
https://docs.expo.io/guides/using-bugsnag/
Bugsnagの導入説明書
https://docs.bugsnag.com/platforms/react-native/expo/

新規登録して、マイページからAPPKey探して、あとはインストールのコマンド打ったら案内してくれたので思ったより導入が楽でした!

PCでiOSのシュミレーターを立ち上げても「There was a problem...」で表示されない

以下サイトに助けてもらいました。
iOSシュミレーター内のiPhoneでアプリをインストールしなおさないといけないとのこと。
iOSシュミレーターの動かし方もググった記憶があります。
参考: Expo Clientアプリにプロジェクトが表示されないときの解決方法

iPhoneSEの小さいデバイスのシュミレーターに変更したい

小さいデバイスでもちゃんと収まるか表示確認したかったので、シュミレーターをiPhoneSEにできないか調べた。
参考1: Is there an iPhone SE simulator for Xcode 11, iOS 13?
参考2: Expo: Change default IOS simulator

上記stack overflowによると、Expoは最後に開いたシミュレーターが次回のシュミレーターになるそうで、

  1. いつも通りシュミレーターを立ち上げているときに、 [ ハードウェア] > [デバイス]をクリックして、目的のデバイスを選択
  2. これで2つのシミュレーターが開く。ほしいシミュレーターがアクティブであることを確認する
  3. Command + Qでシミュレーターアプリを終了
  4. もう一度コマンド「i」でシュミレーターを立ち上げれば、欲しいシュミレーターの方が出るはず

カウント中、勝手にiPhoneがスリープしちゃう問題

KeepAwakeという便利なものを発見して導入しました。
KeepAwake - Expo Documentation

前回実行した筋トレの設定(何分何セット何秒...)の保存をしたい

AsyncStorageの使い方は理解したものの、Pickerの値が変わりませんでした。
孫コンポーネントにcomponentDidMount追加してデータ呼び出してもダメでした。。。

どうやら、データが来る前に描画が走ってしまうっぽいので、データを取得してから描画に行くように、LOADING画面を追加しました。
以下、参考記事です。
React Native AsyncStorage fetches data after rendering

デバッグのために調べた記事
JavascriptのalertでObjectの中身を表示する方法
Reactのライフサイクルを学びなおした記事
React(v16.4) コンポーネントライフサイクルメソッドまとめ

アプリにFirebase導入

expo-firebase-analyticsを使って、以下記事を参考に実装することができました。
つまづきは、Npmのバージョンをあげないといけなかったくらいだったはず。

参考記事一覧:
expo-firebase-analytics: is there a way to enabled debugView? #7745

Using Firebase Analytics with Expo

Using the native Firebase SDK
FirebaseAnalytics

主な流れは以下。

  1. Firebaseプロジェクト作る
  2. プロジェクト内でIOSアプリを追加、
  3. 同じプロジェクト内でWebアプリを追加(これでスタンドアローン?をビルドしなくてもアナリティクス計測できた)
  4. App.jsonにWebアプリのもろもろを書き加える
  5. Promiseでイベント送信するコードを入れる
  6. expo-firebase-analyticsのバージョンをあげる
  7. デバックモードでテスト
  8. expo-firebase-analyticsのバージョンをもどす(expo install expo-firebase-analytics)

アプリをどう使われているか知りたかったので、イベントを導入することに。
以下記事でFirebaseについて学ぶ。

無料で使えるモバイルアプリのデータ計測ツール「Google アナリティクス for Firebase」って何?
Google アナリティクス for Firebaseレポートの見方と10種類のレポートを解説

ほしいイベントをノートに書き出して...

イベントの設計ノート

イベントを設定して、デバッグモードで検知するかテスト。
デバッグモードにするときは、以下を記述しました。

Analytics.setDebugModeEnabled(true);

デバッグモード中のFirebaseの画面
Firebaseの画面

テストフライト

β版テストができると初めて知りました。ここは悩むことなく実行できました。
https://testflight.apple.com

アプリをAppleに申請

何回も設定パターンをテストして、大丈夫!リリースしよう!ってなった時、Google翻訳を使って、まず読み込んだのが以下Expoのドキュメント。
Deploying to App Stores - Expo Documentation

以下2記事は一番参考にしました。
AppStoreの審査でリジェクトされない登録方法 | iTunes Connect
[iPhone] App Store Connect にアプリ情報を登録して申請してみる

アプリ申請のときに記入欄が多く見えて絶望を少しだけ感じましたが、時間かかりながらもなんとかできました。

アプリ申請でやったこと一覧

他に参考にさせていただいた記事(感謝!)

今後翻訳するんだったら、言語設定は日本語にしないほうが良いと書いてありましたが、日本語の音声準備するのがものすごく大変だった(Adobe Auditionで編集しまくった)ので、もう海外対応までやらないだろうと日本語オンリーにしました。

工夫したことは、一通り使用する場面を録画した動画も添付したり、メモ欄にちゃんと使い方などを英語でも書いたことです。
動画は畳の上でやったので、畳がバッチリ写ってたのですが...伝わったんでしょうか。1発OKで通りました。

以下、アプリ申請時のメモ欄に記載したもの。

————————————————
日本語
————————————————

【使い方】
1. 初期設定画面で次のように設定し、「スタート」を押します。
・目標
  「3」回、「3」セット
・インターバル時間
「1」分「00」秒
・間隔(ピッチ)
「3」秒

2. 「5、4、3、2、1」と5秒前からカウントダウンの後、カウントが始まります。
「1、2、3、1回」
「1、2、3、2回」
「1、2、3、3回」
...のように声と表示でカウントします。

3.  1セット目のカウントが終わると「休憩」の声の後、設定したインターバル時間をカウントダウンします。
インターバル時間が終了に近づくと、
「5、4、3、2、1」と5秒前に声のカウントダウンの後、自動で2セット目のカウントが始まります。

4. カウントが終了したら「設定へ」ボタンを押すと、最初の設定画面へ戻ります。

5. カウント中に一時停止したい場合は「一時停止」ボタンを押し、
途中で終了したい場合は「キャンセル」を押してください。

6.最初の設定で、インターバル時間「0分00秒」に設定した場合は、1セット目が終了したときに、インターバルカウントせずにすぐ2セット目のカウントがスタートします。

【注意点】
・消音モードは解除してください。また、イヤホンよりもスピーカーを推奨します。
・ダークモード対応ですが、カウント中にモードを変更すると、設定画面に戻るまで変更されません。
・一時停止のまま10分放置すると自動でカウントがキャンセルされて、最初の設定画面へ戻ります。
・このアプリでは以下の素材を使用させていただいております。どちらも商用利用可能な無料素材です。
・写真素材 足成
http://www.ashinari.com/
・声素材 効果音ラボ
https://soundeffect-lab.info/

————————————————
English
————————————————

【How to use】

1. Please set as follows. And push ‘スタート’ button.
・目標
  「3」回、「3」セット
・インターバル時間
「1」分「00」秒
・間隔(ピッチ)
「3」秒

2. After saying countdown「5、4、3、2、1」, this app starts count.
「1、2、3、1kai」
「1、2、3、2kai」
「1、2、3、3kai」
This app counts like this by voice.

3.  When finishing first set, this app says ’Kyukei’. And this app counts down the first set interval time. This example time is 1 minute.
When the interval time approaches the end, app counts down 「5、4、3、2、1」and 
 Starts counting second set.

4. When the count is over,  Press the ’設定へ’ button to return to the initial setting screen.

5. If you want to pause count, please press ‘一時停止’ button.
If you want to cancel count, please press ‘キャンセル’ button.

6. When the interval time is set to "0" in the first setting, After finishing first count, app skips interval time and starts second count.

【Cautionary point】
・Please cancel silent mode. And I recommend speakers over earphones.
・This app dark mode support, but if you change mode while counting, app will not change until you return to the setting screen
・When you continue for 10 minutes, app cancels the count and return to the initial setting screen automatically.
・This app use those free image and sounds. Both are available for commercial use.
- Picture Ashinari
http://www.ashinari.com/
 - Sounds Soundeffect-lab
https://soundeffect-lab.info/

おわりに

筋トレするときはいつもアプリを使っています。これで筋トレ何回何秒やったか忘れたから途中でやめちゃうなんてことはナシです!!
完全に個人的に作った我流のソースコードなので、たくさん「え?」って思われる実装がたくさんあると思いますが、Reactがどういうものなのかを知ることができて良かったです。
JavaScriptもたくさん調べて、クラス・インスタンス・コンストラクタなど、基礎的なことを学び直すきっかけにもなりました。
そして、これから筋トレを続けて、次の健康診断で悪玉コレステロールの値が下がっていますように...!(>人< ;)

260
205
2

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
260
205