BikeControl (SwiftControl)
Use Zwift Click, Zwift Play, Shimano Di2, or any Bluetooth controller with MyWhoosh, Rouvy, Zwift, and more. Free to try on iOS, Android, Windows, and macOS.
翻訳待ち
No updates detected yet. GitHub releases will appear here.
Strength training for iPhone and Apple Watch, synced with WatchConnectivity.
翻訳待ち
Loading the full README.
Read the full README ↗Small actions. Repeated daily.
Axis is a strength-training app for iPhone and Apple Watch. Build workout routines on your iPhone, run them from your wrist set by set, and see every finished session in your history on the phone. The two apps stay in sync through WatchConnectivity.
Case study: How Axis is designed and built on my portfolio, covering the architecture, code decisions and trade-offs.
Built with SwiftUI, SwiftData, and WatchConnectivity.
iPhone Apple Watch
┌──────────────────────────┐ ┌──────────────────────────┐
│ Create / edit routines │ ── templates ──► │ Pick a routine │
│ (SwiftData) │ application │ Track reps & sets │
│ │ context │ Pause / resume timer │
│ History of completed │ ◄── workout ──── │ Session complete │
│ workouts │ sendMessage / │ │
│ │ transferUserInfo│ │
└──────────────────────────┘ └──────────────────────────┘
Both apps switch between a day theme (7am–7pm) and a night theme automatically. There's no toggle; a timer checks the clock every minute and again whenever the app comes to the foreground.
Theme/AccessibilityModifiers.swiftWorkoutTemplate ──► [TemplateExercise] ──► Exercise (the plan)
CompletedWorkout ──► [CompletedExercise] (what you actually did)
Exercise: an entry in the exercise library (name, equipment, whether it's custom)TemplateExercise: an exercise placed in a routine, with its order and target sets and repsWorkoutTemplate: a named routineCompletedWorkout / CompletedExercise: a snapshot of a finished session. It's stored separately so editing a routine later never changes past history.Only small, lightweight Codable payloads cross the connection. SwiftData models never leave the phone.
| Direction | Payload | Transport | Why |
|---|---|---|---|
| iPhone → Watch | [WatchTemplatePayload] | updateApplicationContext | Only the latest set of routines matters, and the Watch gets it the next time it wakes |
| Watch → iPhone | WatchCompletedWorkoutPayload | sendMessage, falling back to transferUserInfo | Delivered immediately when the phone is reachable; otherwise queued so a finished workout is never lost |
On the phone, PhoneConnectivityManager publishes the incoming workout, and RoutinesHome turns it into a CompletedWorkout and saves it.
WatchWorkoutSession (@Observable) holds all of the in-workout state: rep counting, set completion, moving to the next exercise, the pause and resume clock, and building the payload. The views stay thin, and the logic can be tested without any UI.
Axis/ # iPhone app
├── App/ # AxisApp, RootView (onboarding flow), AppState
├── Core/
│ ├── Models/ # SwiftData models
│ └── Services/ # PhoneConnectivityManager, Watch payloads
├── Features/
│ ├── Launch/ Onboarding/
│ ├── Routines/ # Home, CreateRoutine, RoutineEditor, exercise library
│ └── History/
└── Theme/ # Colors, typography, button style, ThemeManager
AxisWatch Watch App/ # watchOS app
├── Services/ # WatchConnectivityManager, WatchWorkoutSession, theme
└── Views/ # Greeting, WorkoutSelector, StartedWorkout, WorkoutComplete
AxisTests/ # iPhone unit tests
AxisWatch Watch AppTests/ # Watch unit tests
Both targets use Swift Testing (@Suite / @Test), with about 100 tests in total:
CompletedWorkout, the template and completed-exercise models, EquipmentType, and RoutineEditingLogic (exercise filtering and save validation)WatchWorkoutSession from start to finish (rep tracking, completing sets, moving between exercises, the last-exercise check, pausing and resuming, building the payload) and WatchActiveExerciseRun them in Xcode with ⌘U, choosing the iPhone or Watch scheme.
git clone https://github.com/arturoa925/Axis.git
cd Axis
open Axis.xcodeproj
Axis and AxisWatch Watch App targets, and change the bundle IDs (AA.Axis, AA.Axis.watchkitapp) to your own.CompletedWorkout already has totalCalories and averageHeartRate fields ready for thisExercise.isCustom is already in the model)