How big can a Live Activity update be, and what do update and end pushes look like?

17.12.3 · L5 · Interview

Predict first

What sizes result?

import Foundation
import ActivityKit

struct ScoreAttributes: ActivityAttributes {
    struct ContentState: Codable, Hashable { var home: Int; var away: Int; var minute: Int; var note: String }
    var homeTeam: String
    var awayTeam: String
}
func json(_ o: Any) -> Data { try! JSONSerialization.data(withJSONObject: o, options: [.sortedKeys]) }
let state = ScoreAttributes.ContentState(home: 2, away: 1, minute: 67, note: "Goal!")
let encoded = try! JSONEncoder().encode(state)
print("ContentState JSON:", String(data: encoded, encoding: .utf8)!.count, "characters")
let update: [String: Any] = ["aps": ["timestamp": 1_700_000_000, "event": "update",
                                       "content-state": try! JSONSerialization.jsonObject(with: encoded),
                                       "alert": ["title": "Goal", "body": "Pune 2 - 1 Goa"]]]
print("update push bytes:", json(update).count)
let bloated = ScoreAttributes.ContentState(home: 2, away: 1, minute: 67, note: String(repeating: "x", count: 4200))
print("bloated state fits in 4 KB push:", json(["aps": ["timestamp": 1, "event": "update", "content-state": try! JSONSerialization.jsonObject(with: JSONEncoder().encode(bloated))]]).count <= 4096)
let content = ActivityContent(state: state, staleDate: Date(timeIntervalSince1970: 1_700_000_300), relevanceScore: 75)
print("stale date set:", content.staleDate != nil, "relevance:", content.relevanceScore)
let end: [String: Any] = ["aps": ["timestamp": 1_700_000_900, "event": "end", "dismissal-date": 1_700_001_800, "content-state": try! JSONSerialization.jsonObject(with: encoded)]]
print("end payload bytes:", json(end).count)
  • The state is 46 characters, the update 161 bytes, a bloated state does not fit, and the stale date is set
  • Any state fits
  • The update is over 4096 bytes

Mental model. A Live Activity is a scoreboard: you change the numbers on it by telegram, so the telegram must be short.

A Live Activity’s dynamic data is the ContentState, a small Codable struct (46 JSON characters in the run), and an update push carries it under aps as content-state with timestamp and event (update or end), optionally an alert, a stale-date and a dismissal-date. Like all pushes it must fit 4 KB: the sample update is 161 bytes and the end push 137, but a state with a 4200-character note does not fit, so keep the state to numbers and short strings, and put static data (names, ids) in the ActivityAttributes, which never changes after start. ActivityContent adds a staleDate (when the UI should show it is out of date) and a relevanceScore (priority among several activities).

Updates arrive with push type liveactivity and a priority (high ones count against a budget, so reserve them), and activities are limited in number and lifetime (the system ends them after a bounded time). The payload sizes and the types ran in the iOS Simulator; starting a real activity needs a host app with a widget extension and the activities entitlement, and push delivery was not run (areActivitiesEnabled is false in a bare process).

Payload size is limited to 4 KB.

Common traps

  • Putting a document in the state. It must fit in the push.
  • Sending every update at high priority. The budget runs out.

They might follow up with

  • How do I mark data as old? Set a stale date.

Related questions

  • 17.6.1 How do Live Activities (ActivityKit) and WeatherKit work, and what are their limits?

← How do you validate a Sign in with Apple identity token on a server…