Skip to main content

Code generation pipeline

Live Activity models are auto-generated from backend Zod schemas. This ensures the iOS app and backend always agree on the shape of Live Activity payloads.
Never hand-edit LiveActivityModels.generated.swift. Changes will be overwritten on the next generation run. If the model needs to change, update the Zod schema in the backend and regenerate.

Running the generator

# From the backend project root
npm run generate:schemas
This reads the Zod schemas that define Live Activity payloads and outputs a Swift file with matching Codable structs.

Shared between targets

The generated file is included in both the main app target and the widget target (SightscreenWidget). This guarantees that the widget’s Live Activity rendering uses the exact same types as the main app.

Key model types

Schedule models

TypePurpose
MatchItemA single match with pre-formatted display fields (teams, score, status)
LeagueGroupA group of matches under a league header
ScheduleResponseTop-level response from the schedule API — array of LeagueGroup

Live Activity models

TypePurpose
LiveActivityDisplayStateThe full display state for all Live Activity presentations
LiveActivityCompactDisplay data for compact leading and trailing views
LiveActivityExpandedDisplay data for the expanded Dynamic Island and Lock Screen
LiveActivityAttributesStatic data set when the activity is created (match ID, team info)
ContentStateDynamic data updated via APNs pushes (scores, overs, events)
LiveActivityAttributes is set once at creation time and cannot change. ContentState is updated with every push. Design your data split accordingly — anything that changes during a match must be in ContentState.

Auth models

TypePurpose
AppUserThe authenticated user — includes isAdmin flag derived from JWT groups

Core models

TypePurpose
TeamTeam identity — name, abbreviation, logo URL
MatchFull match data from the API
MatchDisplayStatePre-formatted display state for a match (computed by the backend)

Model conventions

All models follow these conventions:
ConventionDetail
CodableAll models conform to Codable for JSON serialization
HashableAll models conform to Hashable for use in SwiftUI lists and diffing
SendableAll models conform to Sendable for safe use across concurrency boundaries
struct MatchItem: Codable, Hashable, Sendable {
    let matchId: String
    let league: String
    let homeTeam: Team
    let awayTeam: Team
    let displayState: MatchDisplayState
    let startTime: Date
}

API format transformation

Models use custom initializers to transform API JSON into app-ready types. The API may return data in a different shape than what the app needs internally.
// Example: init(from decoder:) handles API-specific quirks
init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.matchId = try container.decode(String.self, forKey: .matchId)
    // API sends "start_time" but Swift model uses "startTime"
    // Handled by CodingKeys enum mapping
}
Most transformations are handled automatically by CodingKeys enums that map between snake_case API keys and camelCase Swift properties. Complex transformations live in custom init(from:) implementations.

JSON decoding strategy

The app configures a shared JSONDecoder with ISO 8601 date support that handles both fractional seconds and standard formats.
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom { decoder in
    let container = try decoder.singleValueContainer()
    let dateString = try container.decode(String.self)

    // Try ISO 8601 with fractional seconds first
    let formatterWithFraction = ISO8601DateFormatter()
    formatterWithFraction.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
    if let date = formatterWithFraction.date(from: dateString) {
        return date
    }

    // Fall back to standard ISO 8601
    let formatter = ISO8601DateFormatter()
    formatter.formatOptions = [.withInternetDateTime]
    if let date = formatter.date(from: dateString) {
        return date
    }

    throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(dateString)")
}
The dual-format decoder exists because some backend responses include fractional seconds (e.g., 2024-03-15T10:30:00.000Z) and others do not (e.g., 2024-03-15T10:30:00Z). Both must decode correctly.