No updates detected yet. GitHub releases will appear here.
Loading the full README. The saved excerpt is shown below.
A ride countdown that lives in the Dynamic Island — with no GPS, no network, and no background execution.
You pick where you get on and where you get off, and you tap Start the moment
the train pulls out. A countdown to your station appears on the Lock Screen and in
the Dynamic Island, along with the number of stops left. One station before yours,
the phone buzzes.
On the metro you read, doze off, or put headphones on — and then you look up at
the wrong station. Announcements do not help if you cannot hear them, and there is
no signal down there to check a map.Read the full README ↗A ride countdown that lives in the Dynamic Island — with no GPS, no network, and no background execution.
You pick where you get on and where you get off, and you tap Start the moment the train pulls out. A countdown to your station appears on the Lock Screen and in the Dynamic Island, along with the number of stops left. One station before yours, the phone buzzes.
On the metro you read, doze off, or put headphones on — and then you look up at the wrong station. Announcements do not help if you cannot hear them, and there is no signal down there to check a map.
Three things are unavailable to this app, and only two of them are anyone's fault:
So the train's position cannot be measured. It has to be computed — from a timetable, anchored to the single event a passenger can supply for free: the moment the train starts moving.
That one tap is the whole contract. Everything else follows from it.
UNTimeIntervalNotificationTrigger. They fire even if the app is force-quit
on the platform.Text(timerInterval:). No process has to
stay alive for it to keep ticking.LiveActivityIntents, so a tap
on the Lock Screen wakes the app's own process and reschedules everything.Where the model is uncertain, the passenger is the sensor. A correction takes one tap and never requires unlocking the phone.
Segment times are derived from the official Kyiv Metro timetable published on the city's open data portal — not estimated, not crowdsourced.
Scripts/derive_official_times.py reads the departure times of the first and last
train at every station in both directions. The difference between adjacent stations
is running time plus dwell. That yields up to four samples per segment; the script
takes the median of the plausible ones (60–420 s) and discards outliers produced by
trains starting mid-line from a depot.
At runtime each segment resolves through a fallback chain, and running time and dwell fall back independently — measuring one does not overwrite the other:
| Priority | Source |
|---|---|
| 1 | On-device calibration for this exact direction |
| 2 | Official seed for this direction |
| 3 | Official seed for the mirrored direction |
| 4 | Default: 115 s running, 25 s dwell |
Calibration comes from a dedicated screen where you tap at each stop and start, and passively from GPS while the train is on the surface. Both feed the same running average.
Being explicit about this is part of the design, not a caveat bolted on afterwards.
Waiting for a train after a transfer. This is genuinely unknowable: anywhere from zero to a full headway. The model books a quarter of the current headway rather than the "correct" half, because the two errors do not cost the same. Underestimate, and the countdown runs ahead of the train: the passenger looks up early and keeps riding. Overestimate, and the warning arrives after their station. The whole product exists to prevent the second one. So the estimate leans early on purpose — and a "train departed" button in the Live Activity lets the passenger erase the uncertainty entirely.
The stop counter freezes in the background. With no background modes, ActivityKit cannot be handed a deferred update. The timer keeps running on its own, but "3 stops left" holds its last pushed value until something wakes the app — opening it, tapping ±1, or a GPS fix on the surface. That is the price of the third constraint above, and it was paid knowingly.
| Target | Contents |
|---|---|
App/ | SwiftUI screens, plus services that only make sense in the app: accelerometer, GPS corrector, air-raid polling |
Shared/ | Compiled into both app and widget: models, TripPlanner, TripEngine, ActivityController, NotificationScheduler, strings |
Widget/ | Live Activity presentation only — Dynamic Island compact/minimal/expanded, and the Lock Screen card |
This is the beginning of the README. Read the rest at the original. Read the full README ↗