Skip to main content

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.

Overview

Live Activities are the core user-facing feature of Sightscreen. When a user follows a live match, the app starts a Live Activity that shows ball-by-ball scoring on the Lock Screen and Dynamic Island. All content is driven by backend APNs pushes — the app renders what it receives. The Live Activity system is managed by three dedicated managers, composed behind a LiveActivityService facade.

Three-manager architecture

The original monolithic LiveActivityService has been split into three focused managers, each owning a distinct concern.

LiveActivityTokenManager

Owns everything related to push tokens — observation, deduplication, and backend registration.
ResponsibilityDetail
Push-to-start token observationObserves Activity.pushToStartTokenUpdates and deduplicates double-fire events from the system
Per-activity update token observationObserves pushTokenUpdates on each activity instance
POST tokens to backendCalls POST /matches/:matchId/activity-token whenever a new or rotated token is received
Token cachingCaches the last sent token per match to avoid redundant POSTs

LiveActivityLifecycleManager

Owns the creation, adoption, observation, and teardown of Live Activity instances.
ResponsibilityDetail
Local creationCalls Activity.request() with pushType: .token for user-initiated follows
Adopt push-started LAsObserves Activity.activityUpdates to claim activities the system created via push-to-start
Dismiss observationObserves activityStateUpdates — when state becomes .dismissed, calls POST /matches/:matchId/activity-dismissed
Restore on relaunchRe-attaches observers to all existing activities after a cold start
Stale checkIf no content update arrives for 30 minutes, ends the activity
Content observationWatches contentState updates on each activity for rendering

FollowManager

Owns the user’s follow/unfollow intent and syncs it with the backend.
ResponsibilityDetail
Follow / unfollow API callsPOSTs to the backend when the user taps the bell icon
Local persistenceStores followedMatchIds in UserDefaults for immediate UI state
Retry pending unfollowsQueues failed unfollows and retries on next opportunity
Bell icon statesDrives the three bell states: empty (not following), filled (following), filled + warning (following but LA failed)
Sync on launchCalls GET /user/follows to reconcile local state with the server
The three managers share no mutable state directly. LiveActivityService (the facade) coordinates between them. If you need to change how managers interact, modify the facade — not the managers themselves.

Live Activity lifecycle

Start routes

There are two ways a Live Activity can start:
1

User follow (local creation)

User taps the bell icon. FollowManager records the follow. LifecycleManager calls Activity.request() with pushType: .token. TokenManager begins observing the update token and POSTs it to the backend.
2

Push-to-start (remote creation)

Backend sends a push-to-start APNs notification. The system creates the Live Activity without the app running. On next launch (or in background), LifecycleManager.adoptPushStartedActivity() claims the activity via Activity.activityUpdates. TokenManager then observes and registers the update token.

End reasons

A Live Activity can end for four reasons:
ReasonTriggered by
User unfollowUser taps bell icon again. FollowManager calls unfollow API, LifecycleManager calls Activity.end()
Match overBackend sends APNs push with dismissal-date. Activity transitions to ended state
Stale timeoutLifecycleManager detects no content update for 30 minutes, calls Activity.end()
ActivityKit errorSystem terminates the activity (e.g., resource limits exceeded)

Token management

Three token types

TokenSourcePurpose
Push-to-start tokenActivity.pushToStartTokenUpdates (type-level stream)Allows backend to start a Live Activity remotely
Update tokenactivity.pushTokenUpdates (per-instance stream)Allows backend to update an existing Live Activity via APNs
Device tokenUIApplication delegateStandard push notification token (managed by NotificationService, not TokenManager)

Token rotation

iOS can rotate tokens at any time. TokenManager handles this by:
  1. Continuously observing the async stream (not reading a snapshot)
  2. Comparing each received token against the cached last-sent value
  3. Only POSTing to backend when the token actually changes
// Observing update tokens — TokenManager pattern
for await tokenData in activity.pushTokenUpdates {
    let token = tokenData.hexString
    guard token != lastSentTokens[matchId] else { continue } // deduplicate
    try await api.post("/matches/\(matchId)/activity-token", body: ["token": token])
    lastSentTokens[matchId] = token
}

Push-to-start token deduplication

The system may fire pushToStartTokenUpdates twice for the same token value. TokenManager deduplicates by caching and comparing before sending.
Never try to read activity.pushToken synchronously after creation. The token is not available immediately. Always use the pushTokenUpdates async stream.

Push-to-start flow

1

Pre-follow

The user follows a match before it goes live. FollowManager records the follow and TokenManager registers the push-to-start token with the backend.
2

Backend triggers start

When the match goes live, the backend sends a push-to-start APNs payload using the registered push-to-start token.
3

System creates activity

iOS creates the Live Activity in the background. The app does not need to be running.
4

Adoption

LifecycleManager detects the new activity via Activity.activityUpdates and calls adoptPushStartedActivity() to begin managing it.
5

Token registration

TokenManager observes the update token for the adopted activity and POSTs it to the backend, enabling subsequent content updates.
Push-to-start allows the backend to launch a Live Activity even if the user has not opened the app since following the match. This is the primary start path for pre-followed matches.

Stale activity cleanup

If a Live Activity receives no content-state push for 30 minutes, LifecycleManager considers it stale and ends it. This prevents users from seeing outdated scores (e.g., during a backend outage or if the match is abandoned). The stale check runs:
  • On app foreground (via ScenePhase observation)
  • When restoring activities on relaunch
  • Periodically while the app is active
The 30-minute threshold is a local safety net. The backend should always send an explicit end push when a match completes. Stale detection catches edge cases where that push was lost.

Required presentations

Every Live Activity must implement 6 mandatory presentations. Missing any of these causes a build-time or App Store review rejection.
PresentationWhere it appears
Compact LeadingDynamic Island — left side of the TrueDepth cutout
Compact TrailingDynamic Island — right side of the TrueDepth cutout
MinimalDynamic Island — small circular view when multiple activities are active
ExpandedDynamic Island — long-press or primary activity view
Lock ScreenLock Screen banner below the clock
Watch/CarPlay .smallApple Watch and CarPlay
The compact leading and trailing views appear together when only one Live Activity is running. If multiple are active, iOS picks one for the Minimal presentation and shows the other in Compact.

Content state contract

The backend sends APNs pushes that update the Live Activity content state. The iOS app decodes and renders — no transformation.
RequirementDetail
Key formatcamelCase (matches Swift Codable defaults)
Max payload sizeUnder 4KB (APNs hard limit)
Required fieldstimestamp, event, content-state
APNs push typeliveactivity header for updates
{
  "aps": {
    "timestamp": 1711234567,
    "event": "update",
    "content-state": {
      "matchDisplayState": "live",
      "compactLeading": { "teamAbbr": "MI", "score": "87/6" },
      "compactTrailing": { "overs": "12.3", "runRate": "6.96" },
      "expanded": { /* full expanded display state */ }
    },
    "alert": {
      "title": "Wicket!",
      "body": "Rohit Sharma c Kohli b Bumrah 45(32)"
    }
  }
}

APNs push types

Push typePurpose
liveactivityUpdate or end an existing Live Activity
push-to-startStart a Live Activity remotely without the app being open

Key event handling

Certain events (wickets, milestones) are assigned priority 10 (relevance-score: 10) in the APNs push. This ensures iOS gives them prominence in the Dynamic Island with haptic feedback and expanded presentation.

API endpoints

EndpointMethodPurpose
/matches/:matchId/activity-tokenPOSTRegister an update token for a Live Activity
/matches/:matchId/activity-dismissedPOSTNotify backend that the user dismissed the LA
/user/followsGETSync followed matches on launch
/matches/:matchId/followPOSTFollow a match
/matches/:matchId/unfollowPOSTUnfollow a match

State management summary

StateOwnerStoragePurpose
followedMatchIdsFollowManagerUserDefaultsWhich matches the user is following
lastSentTokensTokenManagerIn-memoryDeduplication cache for token POSTs
Active activity mapLifecycleManagerIn-memoryMaps match IDs to Activity instances
Pending unfollowsFollowManagerIn-memoryQueues failed unfollows for retry
Bell icon stateFollowManagerComputedDerived from follow state + activity health