尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 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.查看完整 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 |
TripEngine is the single owner of trip state. Every mutation — start, manual
correction, GPS snap, finish — goes through it, and each one re-derives the schedule,
reschedules notifications, and pushes a fresh Live Activity state. The widget never
computes anything; it renders a ContentState it was handed.
Both languages live as pairs on one line:
static var lastStopAhead: String { tr("Наступна — ваша", "Yours is next") }
There is no .strings file to fall out of sync, because there is no key to forget.
100 tests over the core, run inside the app process so they read the real bundled data rather than a fixture:
PlannerTests — route composition, monotonic times, dwell placement, transfers,
±1 corrections at both boundaries, notification payloads, Live Activity state stability
(the state must not change mid-segment, or the island would push needlessly)ScheduleTests — headway interpolation inside the hour, weekday vs weekend,
behaviour outside service hours, first/last train warnings, the post-midnight
service day and clock-change daysDataTests — bundled data consistency, coordinates inside a Kyiv bounding box,
surface stations far enough apart for a 400 m GPS snap to be unambiguousLocalizationTests — every sampled string actually changes with the language, and
no Cyrillic survives into the English buildRouteTests — pinned routes and recents (dedupe, caps, migration from older
saves), schedule reminders surviving a round trip, Ukrainian plural forms, station searchMapLayoutTests — every station placed on the schematic map, minimum spacing,
everything inside the canvas, 45°/90° angles, transfer pairs drawn close togetherThe app ships with no analytics, no advertising, no third-party SDKs, and no account. Exactly one feature touches the network — optional air-raid alerts — and it is off until you turn it on.
Everything written to disk (trip journal, calibration, and in Calibration mode the raw
accelerometer and location traces) gets FileProtectionType.completeUnlessOpen and is
excluded from iCloud and iTunes backups. A stolen powered-off phone does not give it up,
and "stays on your device" is a statement about file attributes rather than a promise.
Both targets ship a PrivacyInfo.xcprivacy.
On the App Store since 27 August 2026.
Done: routing with transfers, service hours, Live Activity with interactive corrections, calibration, trip journal, an interactive metro map and schedule reminders (1.1), station exits with directions for all 52 stations (1.2), the extended timetable and alert-level rules of September 2026 (1.3), a wake-up alarm, one-tap position correction, "I'm off" on the Lock Screen, arrival-time sharing and a year-on-the-metro card (1.4), Ukrainian and English throughout, VoiceOver labels, Reduce Motion, privacy policy and support page.
Remaining: field validation. The trip journal records the plan against the outcome and counts every correction precisely so accuracy can be measured rather than asserted — and that data does not exist yet. No accuracy figure is claimed here for that reason.
One open modelling question is worth naming: the headway table encodes each hour as a pair of values that the model interpolates across the hour. Roughly half the hour boundaries in the source data are discontinuous, which suggests the field may instead be a min–max range for the hour. Resolving it against the source layer may shift the transfer-wait estimate.
xcodebuild -project MetroTimer.xcodeproj -scheme MetroTimer \
-destination 'platform=iOS Simulator,name=iPhone 18 Pro Max' test
The Xcode project is committed and has no dependencies — clone and open.
On another machine. git clone [email protected]:dayvan777/MetroTimer.git, open
MetroTimer.xcodeproj, sign in with the team's Apple ID in Xcode → Settings → Accounts;
automatic signing recreates certificates. Build folders are not in git — Xcode rebuilds
them. Uploading to App Store Connect also needs the API key, which is never committed:
copy AuthKey_*.p8 to ~/.appstoreconnect/private_keys/ by hand, or issue a new key in
App Store Connect. On Windows the code, the Python data scripts and the site can be
edited, but building and running the iOS app requires macOS with Xcode. Generators,
data derivation, debug hooks and repository layout are in docs/BUILD.md.
The GPS correction design is in docs/GPS.md.
Release history, App Review lessons and open items are in docs/READINESS.md.
Unofficial app. Not affiliated with or endorsed by Kyiv Metro (КП «Київський метрополітен»).