Skip to main content

Core philosophy

The iOS app is a dumb view layer. The backend owns all formatting, string construction, and business logic. The app’s job is to receive pre-formatted data and render it.
Never compute display strings on the client. Values like "87/6", "RR: 8.50", or "Rohit Sharma 45 (32)" arrive ready-to-render from the API. If you find yourself building a display string in Swift, you are violating the architecture.

What the iOS app does NOT do

  • Format scores, overs, run rates, or player stats
  • Decide what to show based on match state (e.g., innings break vs. live)
  • Apply conditional logic to determine display layouts based on match phase
  • Transform raw data into user-facing text

What the iOS app DOES do

  • Render pre-formatted display states from the backend
  • Manage UI navigation and presentation
  • Handle device-level concerns (Live Activities, push tokens, permissions)
  • Cache and poll for data
  • Send user actions back to the backend

MVVM with @Observable and @Environment DI

The app uses MVVM powered by Swift’s @Observable macro (Observation framework), with dependency injection through @Environment.
Every protocol has two implementations:
  • Real — hits the actual backend API
  • Fake — returns canned data for previews and testing
The app entry point injects the real implementations. SwiftUI previews and tests inject fakes.
ViewModels are @Observable classes, not ObservableObject with @Published. This uses the newer Observation framework available from iOS 17, with backcompat handled where needed.

Facade pattern: LiveActivityService

The most complex subsystem uses the facade pattern. LiveActivityService is a thin coordinator that composes three focused managers: Views and other services only interact with the facade. The three managers are internal implementation details.
The facade prevents views from needing to know which manager handles what. A view calls liveActivityService.follow(matchId:) and the facade coordinates FollowManager + LifecycleManager + TokenManager behind the scenes.

DataSource + ViewSubscriber

A generic pattern for any data that needs to be fetched, cached, and optionally polled.

DataSource protocol

ViewSubscriber modifier

A SwiftUI view modifier that binds a DataSource to a view’s lifecycle.

Concrete data sources


Cache-first reads with stale checking

The app follows a cache-first strategy for all data:
  • MatchCacheStore indexes matches by date with TTL based on match state (live = short TTL, completed = long TTL)
  • ImageCacheService uses a two-tier cache (memory + disk in app group container)
  • ScheduleService uses a sliding-window cache for N days of schedule data
Cache-first means the UI never shows a loading spinner for data the user has already seen. Stale checks happen in the background and the UI updates reactively via @Observable.

Swift 6 actor isolation

The app uses Swift 6 strict concurrency. Key patterns:
All services are @Observable and implicitly @MainActor. If you need to do background work inside a service, use nonisolated methods or explicit Task.detached blocks. Never block the main actor with synchronous network calls.

Data flow

1

Backend returns pre-formatted data

The API responds with display-ready models — formatted strings, ordered arrays, resolved image URLs.
2

Service layer fetches and caches

Protocol-based services make the API call, decode the response, and cache results (via MatchCacheStore or service-local caches).
3

ViewModel exposes state

@Observable view models hold the decoded data. SwiftUI views automatically re-render when properties change.
4

Views render

SwiftUI views bind directly to view model properties. No transformation — just layout and styling.
5

User actions flow back

Taps, follows, and other interactions call service methods, which hit the backend API.

Tech constraints

The iOS 16.1 minimum means you can use NavigationStack, @Observable (with backcompat), and the full ActivityKit API surface.