A native iOS split timer app built with SwiftUI — originally designed for timing golf cart laps, now general purpose for karting, running, or any activity where you want to track split times across multiple checkpoints.
- Tracks & splits — create named tracks with custom split checkpoints (e.g. "T-intersection", "Back straight", "Lap 1")
- Live split timing — tap to split, times update at screen refresh rate via
CADisplayLink - Split colors — purple for overall best, green for personal best, yellow for slower
- Multi-runner support — name each run; personal bests are tracked per name
- History — all runs persisted to Firebase Firestore, synced across devices
- Haptic feedback — medium impact on each split, strong pulse on finish
- Screen always-on — idle timer disabled during active runs
- Background safe — timer stays accurate if app is backgrounded mid-run
- SwiftUI — declarative UI,
@Observable/@ObservedObjectstate management - Firebase Firestore — NoSQL cloud database for cross-device persistence
- CADisplayLink — frame-rate display updates (~120fps on ProMotion) for smooth timer rendering
- UserDefaults — lightweight local persistence for runner name preference
UIImpactFeedbackGeneratorfor hapticsNotificationCenterfor app lifecycle events
ContentView (phase-based navigation)
├── HistoryView — default screen, shows all runs across tracks
├── TrackPickerView — select or create a track before running
└── RunView — active timer, split list, finish sheet
Three view models handle all business logic:
TimerViewModel— owns the CADisplayLink, split recording, and Firestore writeTrackViewModel— track + split definition CRUD, Firestore read/writeHistoryViewModel— loads all runs with splits, computes best times per split definition
Data model:
Track
└── [SplitDefinition] id, name, order
Run
└── [Split] split_at timestamp, split_def_id FK
Tracks own their split definitions as an embedded array in Firestore. Runs and splits are separate top-level collections, with splits fetched and joined client-side after loading runs.
TrackPickerView → select track
→ TimerViewModel(track) initialized with track's SplitDefinitions
→ user taps Start → CADisplayLink begins, startTime = Date()
→ each Split tap → Split(Date(), run.id, splitDef.id) appended
→ finish → timestamps recorded, Firestore write, history reloads
- Clone the repo
- Create a Firebase project at console.firebase.google.com
- Add an iOS app, download
GoogleService-Info.plist, add to Xcode target - Enable Firestore in Firebase console (test mode is fine to start)
- Add Firebase SDK via SPM:
https://github.com/firebase/firebase-ios-sdk - Build & run
| Collection | Key fields |
|---|---|
tracks |
name, created_at, splits[] (embedded) |
runs |
track_id, user_name, started_at, ended_at |
splits |
run_id, split_def_id, split_at |
- Auth (Firebase Auth) to replace name-based personal bests with real user accounts
- Firestore security rules scoped per user
- Live leaderboard view per track
- Apple Watch companion for wrist-tap splits
- Export run data to CSV