Native iOS scoreboard for padel & tennis. Universal app for iPhone and iPad, landscape-locked, full-screen, designed for courtside use with touch or Bluetooth remote.
xcodegen generate # Generate .xcodeproj from project.yml
open PadelPulse.xcodeproj # Build & run in Xcode 16+ for iPadRequires iOS 17.0+. Universal app (iPhone + iPad), landscape-only. Camera features require a real device.
Signing is pinned in project.yml (DEVELOPMENT_TEAM + CODE_SIGN_STYLE: Automatic), so xcodegen regenerations don't wipe it.
One-command redeploy (auto-detects the paired iPad, regenerates, builds, installs, launches):
./ios/scripts/deploy.sh
# or, once symlinked to ~/bin/padelpulse-deploy:
padelpulse-deployNeeded on a free-tier developer account because app signatures expire every 7 days — just plug the iPad in, unlock it, run the script. ~2 min end-to-end.
Manual equivalent (if the script breaks or for a different device):
# With iPad connected via USB and unlocked, find its ID:
xcrun devicectl list devices
# Build + install + launch (replace <DEVICE_ID>):
xcodebuild -project PadelPulse.xcodeproj -scheme PadelPulse \
-destination 'id=<DEVICE_ID>' -configuration Debug -allowProvisioningUpdates build
xcrun devicectl device install app --device <DEVICE_ID> \
~/Library/Developer/Xcode/DerivedData/PadelPulse-*/Build/Products/Debug-iphoneos/PadelPulse.app
xcrun devicectl device process launch --device <DEVICE_ID> com.padelpulse.app- Full padel/tennis scoring: 0 / 15 / 30 / 40 / Deuce / AD
- Golden Point mode (no advantage at 40-40)
- Automatic tiebreaks at 6-6
- Configurable sets: 1, 2, 3, or unlimited
- Undo with full history stack
- Adaptive layout —
LayoutMetricsscales 50+ dimensions from iPhone SE to iPad Pro 13" - Giant score display — readable from across the court
- Team customization — custom names + 8 color presets (Navy, Crimson, Forest, Purple, Teal, Amber, Graphite, Rose)
- Fun random team names — 30 padel-themed names on each new match
- Serve indicator — big gold L/R letter + racket icon paired in the court-side corner of the serving panel, plus a pulsing gold border around the whole panel (readable from across the court)
- Compact set scores — completed sets as pills in the top-right corner
- Match timer — elapsed time from first point
- Swap sides — mirror teams when switching court ends
- Haptic feedback — tactile responses for scoring, games, match events
- Sound effects — system sounds for points, games, match over (toggleable)
- Match state persistence — in-progress match survives app kill and restart
- Animated match-over — confetti particles, staggered entrance, winner glow, trophy fly-in
- Share as image — rendered 600x315 score card via
ImageRenderer - Onboarding overlay — first-launch hints, dismissed permanently
- Serve-pick overlay — before every fresh match, tap a team tile or press its Bluetooth-remote button to set the first server from the court (toggleable in settings)
- Camera overlay — optional PiP camera (opt-in via settings)
- Touch — tap left/right panel to score
- Bluetooth remote — Next/Prev Track for scoring, Play/Pause for undo. While the serve-pick overlay is up, the team buttons pick the first server and Play/Pause flips sides (match the iPad's left/right to reality) — full pre-match setup without walking back to the iPad.
- iPad keyboard — Cmd+Z (undo), Cmd+N (new match), Cmd+Shift+S (swap), Cmd+, (settings), Arrow keys + Space via GCKeyboard
- English, German, Spanish (~95 strings each)
- Runtime language switcher — Auto / EN / DE / ES via Menu in settings, no app restart
PadelPulse/
├── App/PadelPulseApp.swift # @main, scene config, keyboard shortcuts, persistence hooks
├── Models/
│ ├── MatchState.swift # MatchState (Codable) + PadelScoring (stateless logic)
│ ├── SavedMatch.swift # SavedMatch (Codable) + share text builder
│ └── TeamColor.swift # Default colors + Color↔RGB serialization
├── ViewModels/MatchViewModel.swift # @Observable — state, undo stack, persistence, sound
├── Storage/MatchStorage.swift # UserDefaults + Codable match history
├── Views/
│ ├── ScoreBoardView.swift # Root view — panels, toolbar, set pills, overlays
│ ├── TeamPanelView.swift # Team half — giant score, GAMES box, team name
│ ├── SettingsSidebarView.swift # Slide-in settings — pill badges, toggles, language Menu
│ ├── MatchHistoryView.swift # History cards + share (text & image)
│ ├── MatchOverOverlayView.swift # Confetti, staggered entrance, winner glow, share
│ ├── OnboardingOverlayView.swift # First-launch hints
│ ├── ServePickOverlayView.swift # Pre-match "who serves first?" picker (remote-aware)
│ ├── CameraOverlayView.swift # AVFoundation camera + recording
│ ├── MatchTimerView.swift # Match timer pill
│ ├── WallClockView.swift # Current time-of-day pill (HH:mm)
│ ├── CreditsView.swift # Upstream repo + icon attribution
│ └── Components/
│ ├── ColorSwatchPicker.swift # 8-color inline preset picker (cached RGB)
│ ├── ConfettiView.swift # Canvas + TimelineView particle animation
│ ├── MatchScoreCardView.swift # 600x315 share card
│ ├── NameFieldView.swift # Team name text field
│ ├── PadelRacketView.swift # SVG asset, template-tinted to gold (paired with L/R glyph)
│ └── SetScorePill.swift # Set score display pill
├── Services/
│ ├── CameraService.swift # AVCaptureSession management (serial session queue)
│ ├── LanguageService.swift # Runtime locale override (bundle swizzle + Environment locale)
│ ├── RemoteInputService.swift # MPRemoteCommandCenter + GCKeyboard (main-thread hops)
│ └── SoundService.swift # AudioServicesPlaySystemSound + mute toggle
├── Utilities/
│ ├── Constants.swift # Colors + LayoutMetrics (50+ scaled properties)
│ ├── DefaultsKeys.swift # Central registry of every UserDefaults key
│ ├── HapticService.swift # UIImpactFeedbackGenerator wrappers (+ prepareAll)
│ └── ShareImageRenderer.swift # ImageRenderer → UIImage
└── Resources/
├── Assets.xcassets/ # AppIcon, LaunchLogo, DarkBg, GoldColor
├── en.lproj/Localizable.strings
├── de.lproj/Localizable.strings
└── es.lproj/Localizable.strings
+----------------------------+----------------------------+
| [<-][<>][cam][new] | 14:32 01:23 [gear] |
| | S1 6:0 S2 4:3 |
| | |
| +-------+ | +-------+ |
| | GAMES | | | GAMES | |
| | 3 | | | 2 | |
| +-------+ | +-------+ |
| CHIQUITAS | COURT JESTERS |
| 30 | 15 |
| | |
| L🏸 | |
+----------------------------+----------------------------+
↑ gold pulsing border around the serving panel
- Top-left: icon-only toolbar (Undo, Swap, Camera*, New Match) — 44x44pt touch targets
- Top-right: Wall clock + Match timer + Settings (same row), completed set pills below
- Center: two team panels — team name above giant score, GAMES box at inner corner
- Serve side: big gold L/R letter + racket icon paired in the court-side corner of the serving panel (L = deuce-side / left, R = ad-side / right; Spanish renders as I/D — Izquierda/Derecha), plus a pulsing gold border with rounded outer corners around the whole serving panel. Reduce Motion mutes the pulse.
*Camera button only visible when enabled in settings.
- Swift 5.9 / SwiftUI with
@Observablemacro (iOS 17) - AVFoundation for camera preview & video recording
- UserDefaults + Codable for match history + in-progress match persistence
- MPRemoteCommandCenter + GCKeyboard for Bluetooth remote & keyboard input
- AudioServicesPlaySystemSound for sound effects (zero-dependency)
- ImageRenderer for share card generation
- Canvas + TimelineView for confetti particle animation
- XcodeGen for project generation (
project.yml)
xcodegen generate
xcodebuild -project PadelPulse.xcodeproj -scheme PadelPulseTests \
-destination 'platform=iOS Simulator,name=iPad Pro 11-inch (M5)' test- Volume keys cannot be intercepted on iPadOS (OS restriction). Use Media Next/Prev via Bluetooth remote.
- Camera requires a real device (not simulator).
- Universal app (iPhone + iPad), landscape-locked on both devices.
- iPad uses
UIRequiresFullScreen = YES(no Split View/Slide Over). LayoutMetricsuses width-only scaling on iPad (preserving original layout) and dual-axismin(widthScale, heightScale)on iPhone with per-metric min clamps for readability.- Launch screen uses
UILaunchScreendict in Info.plist (not a storyboard). ScoreBoardButtonStyleuses ZStack with fixed 44x44 frame for uniform touch targets.- No network calls, no analytics, no ads, no third-party dependencies.


