Skip to main content

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 via APNs push updates. The backend drives all content — the app renders what it receives.

ActivityKit integration

A Live Activity is started with Activity.request() using pushType: .token to enable remote push updates.
let attributes = LiveActivityAttributes(matchId: matchId, teams: teams)
let initialState = LiveActivityAttributes.ContentState(/* initial display state */)

let activity = try Activity<LiveActivityAttributes>.request(
    attributes: attributes,
    content: .init(state: initialState, staleDate: nil),
    pushType: .token
)
Always use pushType: .token. Without it, the activity cannot receive APNs push updates and will only show the initial state.

Required presentations

Every Live Activity must implement 6 mandatory presentations. Missing any of these causes a build-time or 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 — when multiple activities are active, shows a small circular view
ExpandedDynamic Island — long-press or when the activity is the primary one
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.

Lifecycle

1

Request

App calls Activity.request() with initial attributes and content state. System may reject if limits are exceeded.
2

Active

Activity is visible on Lock Screen and Dynamic Island. Backend sends APNs pushes to update content state.
3

Update

Each push replaces the content state. The app re-renders all 6 presentations with the new data.
4

End

Match ends or user unfollows. Backend sends a push with dismissal-date, or the app calls Activity.end().
5

Stale detection

If no push arrives for 30 minutes, the app’s stale detection mechanism ends the activity to avoid showing outdated scores.
6

Dismiss

After ending, the activity lingers on the Lock Screen for up to 4 hours, then the system removes it.

Push token flow

Push tokens are the bridge between the app and APNs. The flow is asynchronous — you must observe the token stream, not read it synchronously.
// Observing push token updates — the correct way
for await tokenData in activity.pushTokenUpdates {
    let token = tokenData.map { String(format: "%02x", $0) }.joined()
    await sendTokenToBackend(matchId: matchId, token: token)
}
Never try to read activity.pushToken synchronously after creation. The token is not available immediately. Always use the pushTokenUpdates async stream.

Content state contract

The backend sends APNs pushes that update the Live Activity’s content state.
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
push-to-start allows the backend to launch a Live Activity even if the user hasn’t opened the app. This is useful for matches the user has pre-followed.

Key event handling

Certain events (wickets, milestones) are assigned priority 10 in the APNs push. This ensures iOS gives them prominence in the Dynamic Island with haptic feedback and expanded presentation.
{
  "aps": {
    "timestamp": 1711234567,
    "event": "update",
    "relevance-score": 10,
    "content-state": { /* ... */ }
  }
}

State management

The Live Activity service tracks several pieces of local state:
StateStoragePurpose
followedMatchIdsUserDefaultsPersists which matches the user is following across app launches
activeActivityIdIn-memoryMaps match IDs to active Activity instances
teamIntentsIn-memoryTracks which team the user is interested in for a given match
pendingUnfollowsIn-memoryQueues unfollow actions that need to sync with the backend
followedMatchIds is stored in UserDefaults (not Keychain) because it needs to survive app restarts but is not sensitive data.

Stale detection

If a Live Activity receives no push update for 30 minutes, the app considers it stale and ends it. This prevents users from seeing outdated scores if the backend stops pushing (e.g., API outage, match abandoned). The stale check runs:
  • On app foreground
  • On a periodic background task
  • When a new push arrives (resets the timer for that activity)