A SwiftUI app that runs a whole match from your pocket — ball-by-ball scoring, player careers, scorecards and self-contained season portals. Built for gully, club and backyard cricket, no scoring service required.
One native app target, no package manager, no backend — and a surprising amount of depth packed into local-first storage. The numbers that define the project at a glance.
The home screen organizes the whole surface area into six clear routes, each a self-contained module so an in-progress match never collides with a maintenance task. iPad shows a hero panel beside a clean menu; iPhone stacks the same six as full-width cards.
Setup, toss and live scoring — the full lifecycle of a single game.
Roster management and per-player career statistics across all matches.
A browsable archive of completed matches with full scorecards and media.
Attach media to matches and browse it back through a gallery view.
All 42 ICC standard rules, searchable and filterable by category.
Display name, explicit backup & restore via the Files app — no accounts, no sync.
Everything a scorer needs across the life of a match — and the whole season after it. Each surface is a substantial SwiftUI screen backed by its own manager and persistence.
Runs, wickets, extras, strike rotation, bowler changes, over summaries, undo, haptics and chase logic — the heart of the app.
ScoreBoardViewScoreboardStateManagerProfiles with photos, roles and styles. Batting/bowling averages, strike rates and economy are rebuilt from match history, with charts, rankings and head-to-head comparison.
PlayerManagerPlayerStatsChartsDetailed scorecards, media attachments, season grouping by Spring/Summer/Fall/Winter, archiving and full export.
MatchHistoryManagerMatchDetailViewTeam names, captains, overs, roster selection and an animated coin toss that feeds the batting decision back into setup.
CoinTossViewMatchSetupModelProjects a live score snapshot onto a connected external display through a dedicated scene declared in the Info.plist.
LiveScoreProjectionCenterJSON backups of players, matches, media and rules — plus a self-contained HTML season portal with embedded data, avatars and charts.
AppBackupServiceAppHTMLPortalExportServiceSoft-deletes for players, matches, rules and media. Entries are recoverable for 30 days before purge.
RecoveryBinManagerA built-in cricket rulebook plus full create / edit / delete of custom rules, persisted to JSON with legacy migration.
CricketRulesViewCustomRulesManagerCross-entity search over players and matches, with image, CSV, text, JSON and print-optimised export across share sheets.
GlobalSearchViewMatchExportManagerOne tap flags a bowler warning; a second opens a dead-ball runs dialog. Penalties dock −5 or −10, with their own ball and wicket rules.
WarnP5P10Aggregated player and match statistics on demand — an always-visible card on iPad, a full-screen sheet on iPhone to keep the scoring grid clear.
Top PerformersStats CardsIn landscape, a persistent banner shows live runs, wickets, overs and target in large readable type — and anchors the foot of the iPad scoreboard.
CurrentPlayersCardlandscapePrimarily SwiftUI with ObservableObject state managers. It resembles MVVM in places without being a strict implementation — views own real UI state while managers act as view-models and services over a file-backed persistence layer.
State is local and in-memory while the app runs, then saved to JSON, UserDefaults or Keychain. AppCoordinator owns long-lived managers with @StateObject; screens receive them via @ObservedObject, bindings or callbacks.
ScoreboardStateManager holds live match state and nested MatchPerformanceTracker instances per innings.
Lightweight and pragmatic. Views commonly receive managers as initializer parameters. SessionManager accepts optional AuthService, KeychainManager and UserDefaults for testability.
Several services are static or singleton-based — convenient, but a noted limit on testability.
Cricket_Score_BoardAppAppCoordinatorAppDelegateNavigationDestinationScoreBoardViewPlayerDetailViewMatchDetailViewWelcomeView…*View.swiftScoreboardStateManagerPlayerManagerMatchHistoryManagerSessionManagerCustomRulesManagerPlayerMatchRecordMatchSeasonBatsmanPerformanceMatchDeliveryRecordCricketRuleDurableJSONStoreMatchMediaStorageKeychainManagerUserDefaultsAppBackupServiceAppHTMLPortalExportServiceMatchExportManagerRankingsImageGeneratorAppAppearanceMatchSetupDesignSystemRankingsDesignSystemModernBackButtonThe view tree is rooted at AppCoordinator, which dispatches into four self-contained branches. Communication is unidirectional — views read and write state managers, managers reach services, services reach disk; views never touch the data layer directly.
Hero section and menu cards — the entry point to all features.
The full lifecycle of one game, the only multi-step linear flow.
Roster management and per-player career statistics.
A browsable archive of completed matches with media.
The core loop lives in ScoreBoardView and ScoreboardStateManager. Each delivery flows through validation, state snapshots and performance tracking — fully reversible via undo.
scoringAction(_:)Gate the over. Scoring stops the moment max overs, a completed chase, or a chase loss has already happened.
A legal delivery requires a striker, a bowler, and usually a non-striker before runs can be recorded.
saveState()Snapshot score, partnership, active player IDs, tracker arrays, extras and delivery progress — the basis for undo.
addRuns(_:)Increment score and balls, update the partnership, append a delivery token, update batsman & bowler figures, handle over completion, strike changes and match-completion checks.
Wides, no-balls, byes/leg-byes, penalties and dismissal metadata each route through dedicated helpers.
undoLastAction()Restore both scoreboard state and the nested MatchPerformanceTracker data from the saved ScoreboardGameState.
completeMatch() reads the chaseSecond-innings score passes the first. Margin is reported as wickets remaining.
Second-innings score is lower. Margin is reported as the runs difference.
Equal totals resolve to a tie. The completed snapshot is written to MatchRecord.
func addRuns(_ runs: Int) { score += runs balls += 1 if runs == 4 { triggerCelebration(type: "FOUR") } if runs == 6 { triggerCelebration(type: "SIX") } deliveries.append("\(runs)") stateManager.currentPerformanceTracker.recordRuns(runs) if var bowler = stateManager.currentPerformanceTracker.currentBowler { bowler.runsConceded += runs bowler.balls += 1 if bowler.balls >= 6 { // over complete bowler.overs += 1; bowler.balls = 0 speedWarningGivenForCurrentOver = false stateManager.currentPerformanceTracker.endOver() showBowlerSelection = true // prompt next bowler } stateManager.currentPerformanceTracker.currentBowler = bowler } checkMatchCompletion() }
The deliveries scroller speaks a precise visual language. Each ball becomes a colour-coded token so a scorer can verify an over at a glance — runs, extras with bonuses, warnings, penalties and wickets that carry runs.
Overs are integer division of balls by six. Wides, no-balls, warnings and bonuses don't increment the legal ball count.
Strike rotates on odd run totals and odd-run extras, and automatically at the end of every over alongside the bowler-selection prompt.
First tap toggles a Warn flag for the bowler; a second tap opens a dialog to add dead-ball runs without consuming a ball.
P5 docks five runs and consumes a legal ball. P10 docks ten, consumes a ball, and records a wicket.
End of over: the striker changes and the new batter goes to the non-striker's end. Mid-over: the new batter takes strike.
During a chase, RRR is recomputed live from runs remaining over balls left, helping the scorer read the pace of the game.
The iPad layout is the canonical design surface — each flow uses the width as a primary-content / contextual-sidebar split. Here are the six core screens in the order a scorer meets them.
No Core Data, SwiftData, SQLite or network store. Everything is JSON files, UserDefaults, Keychain and copied media — orchestrated by a single resilient store helper.
Writes primary + backup JSON to both Documents and Application Support, then loads the best candidate by non-empty status, save date, item count and priority. Supports legacy UserDefaults migration and lossy decoding to skip corrupt elements.
| Data | Store |
|---|---|
| Players | savedPlayers.jsonlegacy key SavedPlayers |
| Matches | savedMatches.jsonlegacy key savedMatches |
| Custom rules | customCricketRules.json |
| Recovery bin | recoveryBin.json |
| Match media | …/MatchMediaLibrary/<id>/ |
| Archived seasons | UserDefaults archivedMatchSeasonIDs |
| Auth profile | UserDefaults auth.current.profile |
| Session token | Keychain auth.session.token |
| Password verifiers | Keychain auth.local.passwordVerifier.* |
No schema version field — compatibility comes from decodeIfPresent defaults, backup recovery, lossy array decoding, legacy media migration and stat backfills like backfillManOfTheMatchAwards().
No Package.swift, no CocoaPods, no Carthage, no vendored binaries. The whole app stands on the system SDK.
async/await drives auth, media import, share/export and splash. Task {} launches from SwiftUI actions, SessionManager is @MainActor, and DispatchQueue.main handles animation timing. No custom actors or operation queues.
Keychain items use kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly; passwords are stored as SHA256(salt:password) verifiers, never plaintext. Auth is local-only with explicit backend integration points — better than plaintext, not production remote auth.
Scoring has to be right the first time. These are the non-functional commitments that keep the book accurate, fast and usable for everyone.
Manual sequences plus Swift Testing cover every scoring path — extras, warnings, penalties, undo integrity and innings transitions.
SwiftUI diffing minimises redraws; undo serialization and stat computation run asynchronously, and the deliveries scroller lazy-loads.
VoiceOver announces the striker, bowler, deliveries and warnings; controls carry descriptive labels and meet contrast and dynamic-type guidelines.
All data stays on-device with no network calls. Inputs are validated to prevent malformed state, and writes are guarded against corruption on suspension.
A modular codebase with documented methods and structured tests, so new delivery types or screens attach without disturbing existing logic.
The edge cases scorers actually hit during a match.
The current match state persists automatically. On relaunch the app resumes from the last recorded ball, with every player's stats intact.
Wides, no-balls, warnings and bonus runs are extras or special cases — they add to the score but don't increment the legal ball count, so the over isn't shortened.
Bonus runs don't consume a ball and don't rotate the strike. Odd runs off the bat — and odd-run extras — do switch the striker.
Undo restores the entire state snapshot from just before the action — score, balls, deliveries and every batsman and bowler figure — so complex actions revert cleanly.
No. The app prompts for the required player selections before a legal delivery so stats and roles stay accurate.
Yes — Settings → Export Records writes a single backup file you can share via the Files app. On another device, Import Records merges players, matches and media into the local database.
It divides the runs still needed by the balls remaining (converted to overs), recomputed live through the second-innings chase.
They're supported on iOS devices with the hardware for it, and degrade gracefully where that support isn't present.
The documentation doesn't pretend the codebase is finished. A handful of very large SwiftUI files concentrate logic that would be easier to test if extracted.
How cricket's vocabulary maps onto the codebase.