
前回、回数を数えるだけの筋トレアプリをSwiftUIで自作しました。
記録も分析もなし。とにかく「トレ中に邪魔しない」を最優先にしたやつです。
前回の続きとして、今回は 回数カウント+休憩タイマーを“ワンタップ運用”に寄せた話を書きます。
Contents
導入:筋トレ中は「次に何押すんだっけ?」が一番の敵
筋トレ中って、画面を見る余裕がないことが多いです。
- 手が塞がる
- 息が上がる
- 汗でスマホ触りたくない
この状態で、ボタンが複数あると一気に使われなくなります。
結論はシンプルで、1画面+1ボタンで状態を回すのが一番ラクでした。
この記事で得られることは以下です。
- ワンタップ化のための「状態設計(最小)」の考え方
- 回数カウントと休憩タイマーをつなぐ最小コード
- 誤タップや停止漏れを減らすための実装ポイント
前提:多機能化はしない(今回も割り切り全振り)
今回の方針は前回と同じです。
- 画面遷移なし
- 設定なし(秒数カスタムはしない)
- 永続化なし
- バックグラウンド対応はしない
筋トレ中に欲しいのは、正確さより「邪魔しないこと」。
ここからブレると、また使われなくなります。
問題の正体:なぜ操作が迷いになるのか
ワンタップ化の前に、何が邪魔しているかを分解します。
- ボタンが複数ある → 判断が増える
- 画面が分かれる → 遷移が増える
- 状態が見えない → いまトレ中?休憩中?が曖昧
- 停止漏れが起きる → タイマーが裏で動き続ける事故
つまり、筋トレ中のアプリで一番避けたいのは 判断 と 忘れ物 です。
解決策:状態を2つに潰して、メイン操作を1ボタンに寄せる
1) 状態は2つだけ(training / rest)
状態が増えるほど、ボタンと分岐が増えます。
なので、今回は割り切ってこうします。
- training(トレ中)
- rest(休憩中)
表示も動作も、この2つだけで回します。
2) メイン操作を「Nextボタン」1つに固定する
筋トレ中にやりたいのは、だいたいこれだけです。
- セットが終わった → 休憩開始したい
- 休憩が終わった → 次セットへ行きたい
なら、メイン操作は1つでいいです。
- トレ中に押す → 休憩開始(タイマーStart)
- 休憩中に押す → 次セットへ(タイマーStop)
ボタンのラベルは状態で変えます。
- training:休憩開始
- rest:次セットへ
これだけで「次に何押すか」が消えます。
3) 停止漏れはUIで防がず、コード側で潰す
停止ボタンを置くと、押し忘れます。
なので、停止は「状態遷移で必ず走る」ように寄せます。
- training に戻るタイミングで
invalidate - 画面が消えるときも
invalidate
筋トレ中に“自分の注意力”に頼る設計は負けです。
コードサンプル:回数+休憩をワンタップで回す最小構成
状態定義
import SwiftUI
import AudioToolbox
enum Mode {
case training
case rest
}
struct ContentView: View {
@State private var mode: Mode = .training
@State private var repCount: Int = 0
@State private var restSeconds: Int = 60
@State private var remainingTime: Int = 60
@State private var timer: Timer?
メインボタン(これが主役)
private func handleMainButtonTap() {
switch mode {
case .training:
// 休憩開始
mode = .rest
startRestTimer()
case .rest:
// 次セットへ
stopTimer()
mode = .training
remainingTime = restSeconds
// 必要なら回数をリセット(方針次第)
// repCount = 0
}
}
タイマー開始・停止+音
private func startRestTimer() {
stopTimer()
remainingTime = restSeconds
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
if remainingTime > 0 {
remainingTime -= 1
} else {
stopTimer()
playSound()
// 休憩終了で自動遷移させるかは好み。
// 今回は「ワンタップ運用」を優先して自動遷移はしない。
}
}
}
private func stopTimer() {
timer?.invalidate()
timer = nil
}
private func playSound() {
AudioServicesPlaySystemSound(1005)
}
1画面にまとめる(表示はmodeで切替)
var body: some View {
VStack(spacing: 16) {
Text(mode == .training ? "トレーニング" : "休憩")
.font(.headline)
if mode == .training {
Text("\(repCount)")
.font(.system(size: 72, weight: .bold))
} else {
Text("\(remainingTime) 秒")
.font(.system(size: 56, weight: .bold))
}
HStack(spacing: 12) {
Button("+1") { repCount += 1 }
.buttonStyle(.bordered)
Button("−1") { repCount = max(0, repCount - 1) }
.buttonStyle(.bordered)
}
Button(mode == .training ? "休憩開始" : "次セットへ") {
handleMainButtonTap()
}
.buttonStyle(.borderedProminent)
Button("リセット") {
stopTimer()
mode = .training
repCount = 0
remainingTime = restSeconds
}
.buttonStyle(.bordered)
}
.padding()
.onDisappear {
stopTimer()
}
}
}
これで、運用はこうなります。
- セット終わりに「休憩開始」
- 音が鳴ったら「次セットへ」
筋トレ中に考えることが増えないのが一番の勝ちです。
チェックリスト(実装で守るルール)
- 状態は training/rest の2つだけ
- 画面遷移なし、1画面で完結
- メイン操作は1ボタン(状態でラベル変更)
- 状態が戻る経路で timer を必ず止める
- 画面破棄でも timer を止める(onDisappear)
具体例(筋トレ中の運用フロー)
筋トレ中の操作は、これだけで回ります。
- セットが終わる → 休憩開始
- 休憩が終わる(音が鳴る) → 次セットへ
- 回数は「+1」だけで増やす
「次に何を押すか」を考えない設計が、継続に直結します。
次にやるなら(でも、必要になるまでやらない)
ワンタップ運用を壊さずに伸ばすなら、次はこの程度が限界です。
- 秒数を3パターン固定で切替(自由入力は沼)
- トレ開始をもっと雑に(ボタン配置の最適化)
ただ、必要になるまでやりません。
筋トレアプリは“育てる”より“使われる”が正義です。
関連記事
筋トレアプリがサービス終了。回数を数えるだけのiOSアプリを自作した話
SwiftUIで作る筋トレアプリ|回数カウント+音だけのインターバルタイマーを追加した話
おわりに
筋トレ中に欲しいのは、機能ではなく 迷いゼロ です。
状態を2つに潰して、メイン操作を1ボタンに寄せる。
これだけで、回数カウント+休憩タイマーが「現場で使える形」になります。
本日はここまで。では、また次の記事でお会いしましょう。



--300x169.png)