専用アプリを作ってリレーマラソンの大会に参加してきました

こんにちは。iOS アプリエンジニアの id:gurrium です。あるいは、ARMC(Arashiyama Relay Marathon Club) の id:gurrium です。

ARMC というのは、嵐山リレーマラソンに参加したい人達が集まってできた非公式のクラブです。

今までは各自で練習をしているだけでしたが、去る1月11日、本来の目的を果たすべく「京都嵐山耐寒開運リレーマラソン」に参加しました。 taikan.kyoto-ekiden.net

リレーマラソンというのは、複数人で交代しながら決められた距離を走る競技です。誰がどれだけ走るか事前に計画はしますが、ARMC のほとんどは半年前にジョギングを始めた程度の初心者であり、ペースもコンディションも安定しません。そのため、当日の状況に合わせて計画を変えることが予想されました。これは紙とペンでやるには難しいですし、スプレッドシートもスマートフォンから操作するのには向いていません。そこで、専用のアプリを作ることにしました。

機能紹介

レギュレーション設定

周回数と制限時間を設定できます。嵐山リレーマラソンでは同じコースを周回するので距離ではなく周回数にしています。

レギュレーションの設定画面。周回数が「42」、制限時間(分)が「250」に設定されている。

走行計画

各ランナーがどれくらいのペースで何周走るかを設定します。

ランナーのリスト。「cat」「duck」などの名前とともに、各ランナーのペース(/km)と担当周回数が設定されている。

CSVをインポートできるので、事前にスプレッドシートなどを使って計画しておくこともできます。

CSVインポート画面。ファイルの入力形式(名前,周回数,タイム)の指示と、読み込まれたランナーのプレビューリストが表示されている

計測・表示

レース中は以下の情報をリアルタイムで表示します。

現在のランナーの情報

  • 残り時間
  • 残り周回数
  • 計画

全体の情報

  • 完走に必要なペースと周回数
  • 経過時間 / 制限時間
  • 現在の周回数 / 総周回数

現在のランナーのペースを表示していないのは、リレーマラソンでは1人が数周しか走らず、1周ごとにしか計れないペースを表示する意味が薄いためです。また、経過時間ではなく残り時間なのは、計測する側が次のランナーの準備をしたり現在のランナーに残り時間を伝えて発破をかけたりするのに便利だからです。

計測中も走行計画を編集できます。

計測中のメイン画面。現在のラップタイム、残りの周回数(42周)や予測タイムが表示され、下部には次のランナーのリストが並んでいる

レース結果の保存

レース終了時に全ての情報をJSONで出力します。保存機能が無いことに気づいたのが本番直前だったので表示は実装しませんでした。AIがなんとかしてくれると信じて。

後日AIにいい感じにしてもらいました(ベストラップが1秒なのは計測を忘れたラップを飛ばしたから)。

リレーマラソンの結果を可視化したダッシュボード。「RELAY DASHBOARD」のタイトルの下に、合計タイム(3:26:52)、平均ラップ(4:55)、参加人数(12名)などの主要な記録が並んでいる。画面中段にはラップタイムの推移グラフとランナーごとの詳細、下段には各メンバーの周回数貢献度を示す横棒グラフが配置されている

技術的な話

Duration

経過時間やラップタイムには Duration という型を使っています。時間を表す型は TimeInterval しか知らなかったのですが、見かけたので使ってみました。

今回の用途では TimeInterval より使いやすかったです。精度はどちらでも十分ですが、Duration の方がフォーマッタが便利なので表示周りのコードがシンプルになります。

例えばペースの時間部分を表すのに使っている m:ss という表記に必要なコードはこれだけです。

duration.formatted(.time(pattern: .minuteSecond))

経過時間の H:mm:ss.SSS は少し長くなりますが、 TimeInterval のために DateComponentsFormatter をこねこねするよりは楽ですし、生成コストを気にする必要もありません。

duration.formatted(
    .time(
        pattern: .hourMinuteSecond(
            padHourToLength: 0,
            fractionalSecondsLength: 3
        )
    )
)

このように簡単にフォーマットできるのは DurationFormatStyle に準拠した Duration.TimeFormatStyle を提供しているためです。

Duration にはもう1つ Duration.UnitsFormatStyle というのもあって、こちらは単位付きの表記ができます。

var style = Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes, .seconds], width: .wide)
duration.formatted(style) // "23 hours, 7 minutes, 22 seconds"
style.locale = .init(identifier: "ja_JP")
duration.formatted(style) // "23時間 7分 22秒"

ちなみに、FormatStyle を提供している型は他にもたくさんあるのでフォーマットしたくなったときは調べてみるとよいかもしれません。おすすめの資料はこちらです。 speakerdeck.com

今後

当初、我々の目標は制限時間(4時間10分)内での完走でしたが、それを大きく上回る3時間半弱でゴールしました。それもあり、他のチームのタイムに影響されたのもあり、打ち上げの頃には来年は3時間を切るぞと盛り上がっていました。そのためには、1人1周で交代するのがよいだろうとか、走行計画を管理する人を専属で置くのはどうかとか様々な意見が出ているので、それに合わせてアプリも変えていくつもりです。

ひとまず、ミリ秒を気にするレベルになるまでは画面の更新頻度を毎秒1回に落とします。バッテリーがもりもり減って大変だったので…。