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 aLiveActivityService facade.
Three-manager architecture
The original monolithicLiveActivityService has been split into three focused managers, each owning a distinct concern.
LiveActivityTokenManager
Owns everything related to push tokens — observation, deduplication, and backend registration.| Responsibility | Detail |
|---|---|
| Push-to-start token observation | Observes Activity.pushToStartTokenUpdates and deduplicates double-fire events from the system |
| Per-activity update token observation | Observes pushTokenUpdates on each activity instance |
| POST tokens to backend | Calls POST /matches/:matchId/activity-token whenever a new or rotated token is received |
| Token caching | Caches the last sent token per match to avoid redundant POSTs |
LiveActivityLifecycleManager
Owns the creation, adoption, observation, and teardown of Live Activity instances.| Responsibility | Detail |
|---|---|
| Local creation | Calls Activity.request() with pushType: .token for user-initiated follows |
| Adopt push-started LAs | Observes Activity.activityUpdates to claim activities the system created via push-to-start |
| Dismiss observation | Observes activityStateUpdates — when state becomes .dismissed, calls POST /matches/:matchId/activity-dismissed |
| Restore on relaunch | Re-attaches observers to all existing activities after a cold start |
| Stale check | If no content update arrives for 30 minutes, ends the activity |
| Content observation | Watches contentState updates on each activity for rendering |
FollowManager
Owns the user’s follow/unfollow intent and syncs it with the backend.| Responsibility | Detail |
|---|---|
| Follow / unfollow API calls | POSTs to the backend when the user taps the bell icon |
| Local persistence | Stores followedMatchIds in UserDefaults for immediate UI state |
| Retry pending unfollows | Queues failed unfollows and retries on next opportunity |
| Bell icon states | Drives the three bell states: empty (not following), filled (following), filled + warning (following but LA failed) |
| Sync on launch | Calls GET /user/follows to reconcile local state with the server |
Live Activity lifecycle
Start routes
There are two ways a Live Activity can start: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.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:| Reason | Triggered by |
|---|---|
| User unfollow | User taps bell icon again. FollowManager calls unfollow API, LifecycleManager calls Activity.end() |
| Match over | Backend sends APNs push with dismissal-date. Activity transitions to ended state |
| Stale timeout | LifecycleManager detects no content update for 30 minutes, calls Activity.end() |
| ActivityKit error | System terminates the activity (e.g., resource limits exceeded) |
Token management
Three token types
| Token | Source | Purpose |
|---|---|---|
| Push-to-start token | Activity.pushToStartTokenUpdates (type-level stream) | Allows backend to start a Live Activity remotely |
| Update token | activity.pushTokenUpdates (per-instance stream) | Allows backend to update an existing Live Activity via APNs |
| Device token | UIApplication delegate | Standard push notification token (managed by NotificationService, not TokenManager) |
Token rotation
iOS can rotate tokens at any time.TokenManager handles this by:
- Continuously observing the async stream (not reading a snapshot)
- Comparing each received token against the cached last-sent value
- Only POSTing to backend when the token actually changes
Push-to-start token deduplication
The system may firepushToStartTokenUpdates twice for the same token value. TokenManager deduplicates by caching and comparing before sending.
Push-to-start flow
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.Backend triggers start
When the match goes live, the backend sends a push-to-start APNs payload using the registered push-to-start token.
System creates activity
iOS creates the Live Activity in the background. The app does not need to be running.
Adoption
LifecycleManager detects the new activity via Activity.activityUpdates and calls adoptPushStartedActivity() to begin managing it.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
ScenePhaseobservation) - 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.| Presentation | Where it appears |
|---|---|
| Compact Leading | Dynamic Island — left side of the TrueDepth cutout |
| Compact Trailing | Dynamic Island — right side of the TrueDepth cutout |
| Minimal | Dynamic Island — small circular view when multiple activities are active |
| Expanded | Dynamic Island — long-press or primary activity view |
| Lock Screen | Lock Screen banner below the clock |
Watch/CarPlay .small | Apple 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.| Requirement | Detail |
|---|---|
| Key format | camelCase (matches Swift Codable defaults) |
| Max payload size | Under 4KB (APNs hard limit) |
| Required fields | timestamp, event, content-state |
| APNs push type | liveactivity header for updates |
APNs push types
| Push type | Purpose |
|---|---|
liveactivity | Update or end an existing Live Activity |
push-to-start | Start 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
| Endpoint | Method | Purpose |
|---|---|---|
/matches/:matchId/activity-token | POST | Register an update token for a Live Activity |
/matches/:matchId/activity-dismissed | POST | Notify backend that the user dismissed the LA |
/user/follows | GET | Sync followed matches on launch |
/matches/:matchId/follow | POST | Follow a match |
/matches/:matchId/unfollow | POST | Unfollow a match |
State management summary
| State | Owner | Storage | Purpose |
|---|---|---|---|
followedMatchIds | FollowManager | UserDefaults | Which matches the user is following |
lastSentTokens | TokenManager | In-memory | Deduplication cache for token POSTs |
| Active activity map | LifecycleManager | In-memory | Maps match IDs to Activity instances |
| Pending unfollows | FollowManager | In-memory | Queues failed unfollows for retry |
| Bell icon state | FollowManager | Computed | Derived from follow state + activity health |