Live Activity for Crypto Price Tracking on iOS
Live Activities appeared in iOS 16.1 and provide a fundamentally different way to show current data — not just a widget on Home Screen, but a constantly updated tile on Lock Screen and in Dynamic Island (iPhone 14 Pro and newer). For trading apps and crypto wallets, this is an entry point users see without opening the app.
ActivityKit and limitations
Live Activity is created via ActivityKit. Important to understand the fundamental limitation before starting: Activity can only be started from the app itself when it's in foreground. Can't start Activity from background process or push notification. Can update — via ActivityKit API or push (ActivityKit Push Update).
Maximum lifetime of one Activity — 12 hours (system can end earlier). ActivityAttributes data — static for entire lifetime. ContentState data — dynamic, these are what get updated.
For a crypto widget, architecture looks like:
struct CryptoActivityAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
var price: Double
var change24h: Double
var lastUpdated: Date
}
var symbol: String // static: BTC, ETH, etc.
var baseCurrency: String // static: USD
}
Starting and updating
let attributes = CryptoActivityAttributes(symbol: "BTC", baseCurrency: "USD")
let initialState = CryptoActivityAttributes.ContentState(
price: 67_430.0,
change24h: 2.3,
lastUpdated: .now
)
let activity = try Activity<CryptoActivityAttributes>.request(
attributes: attributes,
content: .init(state: initialState, staleDate: Date().addingTimeInterval(60)),
pushType: .token // if planning to update via push
)
staleDate — moment when system considers data stale and can show special UI. For crypto price set to 60–120 seconds.
Updating via local code:
let updatedState = CryptoActivityAttributes.ContentState(
price: newPrice,
change24h: newChange,
lastUpdated: .now
)
await activity.update(.init(state: updatedState, staleDate: Date().addingTimeInterval(60)))
Updating via ActivityKit Push
For real-time price updates need a backend sending ActivityKit Push Notification — separate type of push, not APNs notification. Payload looks like:
{
"aps": {
"timestamp": 1699000000,
"event": "update",
"content-state": {
"price": 68100.0,
"change24h": 2.8,
"lastUpdated": 1699000000
},
"alert": {
"title": "BTC",
"body": "$68,100"
}
}
}
Token for ActivityKit Push — separate from regular APNs token. App gets it via activity.pushTokenUpdates and must send to server. If token doesn't update on server after Activity restart — updates stop arriving.
Dynamic Island: compact and expanded views
SwiftUI layout for Dynamic Island divides into several presentations: compactLeading, compactTrailing, minimal, expanded. Each — separate SwiftUI View. Size limitation for compact views is strict — literally a few pixels, no lists.
.dynamicIsland { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Text(context.attributes.symbol).font(.headline)
}
DynamicIslandExpandedRegion(.trailing) {
Text(context.state.change24h >= 0 ? "↑" : "↓")
.foregroundColor(context.state.change24h >= 0 ? .green : .red)
}
DynamicIslandExpandedRegion(.center) {
Text("$\(context.state.price, format: .number.precision(.fractionLength(2)))")
.font(.title2)
}
} compactLeading: {
Text(context.attributes.symbol).font(.caption2)
} compactTrailing: {
Text("$\(Int(context.state.price))").font(.caption2)
} minimal: {
Text(context.attributes.symbol.prefix(1))
}
}
What's included in the work
- Creating ActivityKit extension with
ActivityAttributesandContentState - SwiftUI layout for Lock Screen, Dynamic Island (compact, minimal, expanded)
- Starting and ending Activity from main app
- Setting up ActivityKit Push updates (requires server part)
- Handling data staleness (
staleDate) - Testing on iPhone with and without Dynamic Island
Timeline
2–3 days for UI part with local updates. Server integration for push updates — plus 1–2 days. Cost calculated individually after requirements analysis.







