> ## Documentation Index
> Fetch the complete documentation index at: https://285e39fd5e337e58f16290.sightscreen.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Architecture

> iOS app architecture — server-driven UI, MVVM with @Observable, facade pattern, DataSource + ViewSubscriber, cache-first reads, and Swift 6 actor isolation.

## 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.

<Warning>
  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.
</Warning>

### 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`.

| Layer         | Responsibility                                                                             |
| ------------- | ------------------------------------------------------------------------------------------ |
| **View**      | SwiftUI views that read from observable models and render UI                               |
| **ViewModel** | `@Observable` classes that hold view state and call services                               |
| **Service**   | Protocol-based services injected via `@Environment`, each with Real + Fake implementations |
| **Backend**   | Source of truth for all data and display formatting                                        |

```swift theme={null}
// Accessing a service in a view
struct MatchListView: View {
    @Environment(\.scheduleService) private var scheduleService

    var body: some View {
        // ...
    }
}
```

```swift theme={null}
// Defining the environment key
private struct ScheduleServiceKey: EnvironmentKey {
    static let defaultValue: ScheduleService = RealScheduleService()
}

extension EnvironmentValues {
    var scheduleService: ScheduleService {
        get { self[ScheduleServiceKey.self] }
        set { self[ScheduleServiceKey.self] = newValue }
    }
}
```

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.

<Note>
  ViewModels are `@Observable` classes, not `ObservableObject` with `@Published`. This uses the newer Observation framework available from iOS 17, with backcompat handled where needed.
</Note>

***

## Facade pattern: LiveActivityService

The most complex subsystem uses the **facade pattern**. `LiveActivityService` is a thin coordinator that composes three focused managers:

```mermaid theme={null}
flowchart TB
    Views -->|"@Environment(\.liveActivityService)"| Facade[LiveActivityService]
    Facade --> TM[LiveActivityTokenManager<br/>tokens + deduplication]
    Facade --> LM[LiveActivityLifecycleManager<br/>create + adopt + observe + end]
    Facade --> FM[FollowManager<br/>follow state + API + bell icons]
```

Views and other services only interact with the facade. The three managers are internal implementation details.

| Manager            | Single responsibility                                                                 |
| ------------------ | ------------------------------------------------------------------------------------- |
| `TokenManager`     | Push-to-start + update token observation, deduplication, backend registration         |
| `LifecycleManager` | `Activity.request()`, adopt push-started LAs, state observation, stale check, restore |
| `FollowManager`    | Follow/unfollow API, local persistence, retry, bell icon state, sync from server      |

<Tip>
  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.
</Tip>

***

## DataSource + ViewSubscriber

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

### DataSource protocol

```swift theme={null}
protocol DataSource<T> {
    associatedtype T

    var data: T? { get }

    func load() async throws -> T       // cache-first
    func refresh() async throws -> T    // force fetch
    func shouldPoll() -> Bool
    var pollIntervalMs: Int { get }
}
```

### ViewSubscriber modifier

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

```swift theme={null}
MatchListView()
    .viewSubscriber(dataSource: liveMatchesDataSource)
```

| Behavior             | Detail                                                 |
| -------------------- | ------------------------------------------------------ |
| Lifecycle binding    | `onAppear` starts polling, `onDisappear` stops         |
| ScenePhase awareness | Pauses in background, resumes on foreground            |
| Cache-first          | Shows cached data immediately, refreshes in background |

### Concrete data sources

| DataSource              | Behavior                                                                       |
| ----------------------- | ------------------------------------------------------------------------------ |
| `LiveMatchesDataSource` | Always polls. `pollIntervalMs` is server-driven via `retryAfterMs` in response |
| `ScheduleDataSource`    | Per-date caching. Only polls for today's date (historical dates are static)    |

***

## Cache-first reads with stale checking

The app follows a **cache-first** strategy for all data:

```mermaid theme={null}
flowchart LR
    Request --> Cache{Cache hit?}
    Cache -->|Yes| Return[Return cached data]
    Cache -->|No| Fetch[Fetch from API]
    Return --> Stale{Stale?}
    Stale -->|Yes| Fetch
    Stale -->|No| Done
    Fetch --> Update[Update cache]
    Update --> Done
```

* `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

<Note>
  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`.
</Note>

***

## Swift 6 actor isolation

The app uses Swift 6 strict concurrency. Key patterns:

| Pattern                | Where used                                                          |
| ---------------------- | ------------------------------------------------------------------- |
| `@MainActor`           | All `@Observable` classes, ViewModels, and UI-bound state           |
| `Sendable` conformance | All model types (generated and hand-written)                        |
| `nonisolated` methods  | Pure computation and encoding/decoding that does not touch UI state |
| Structured concurrency | `Task {}` blocks scoped to view lifecycle via `ViewSubscriber`      |

<Warning>
  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.
</Warning>

***

## Data flow

```mermaid theme={null}
flowchart LR
    A[Backend API] -->|JSON response| B[Service Layer]
    B -->|Fetch + cache| C["@Observable ViewModel"]
    C -->|State binding| D[SwiftUI Views]
    D -->|User action| E[Service Layer]
    E -->|API call| F[Backend API]
```

<Steps>
  <Step title="Backend returns pre-formatted data">
    The API responds with display-ready models — formatted strings, ordered arrays, resolved image URLs.
  </Step>

  <Step title="Service layer fetches and caches">
    Protocol-based services make the API call, decode the response, and cache results (via `MatchCacheStore` or service-local caches).
  </Step>

  <Step title="ViewModel exposes state">
    `@Observable` view models hold the decoded data. SwiftUI views automatically re-render when properties change.
  </Step>

  <Step title="Views render">
    SwiftUI views bind directly to view model properties. No transformation — just layout and styling.
  </Step>

  <Step title="User actions flow back">
    Taps, follows, and other interactions call service methods, which hit the backend API.
  </Step>
</Steps>

***

## Tech constraints

| Constraint          | Value                                                        |
| ------------------- | ------------------------------------------------------------ |
| UI framework        | SwiftUI only (no UIKit)                                      |
| Minimum iOS version | 16.1                                                         |
| Reason for iOS 16.1 | Live Activities require ActivityKit, available from iOS 16.1 |
| Concurrency model   | Swift 6 strict concurrency                                   |
| Architecture        | MVVM with `@Observable`                                      |
| DI mechanism        | `@Environment` with protocol-based services                  |
| Data format         | All display data pre-formatted by backend                    |

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