Before Live Activities, showing live progress outside your own app — a delivery, a live score, a timer — meant either the user keeping the app foregrounded or you spamming local notifications; nothing occupied the Lock Screen persistently and updated in place.
#The new way
import ActivityKit
struct DeliveryAttributes: ActivityAttributes {
struct ContentState: Codable, Hashable {
var driverName: String
var minutesAway: Int
}
var orderNumber: String
}
func startDeliveryActivity(orderNumber: String) throws {
let attributes = DeliveryAttributes(orderNumber: orderNumber)
let initialState = DeliveryAttributes.ContentState(driverName: "Alex", minutesAway: 18)
_ = try Activity<DeliveryAttributes>.request(
attributes: attributes,
content: .init(state: initialState, staleDate: nil)
)
}
func updateDeliveryActivity(_ activity: Activity<DeliveryAttributes>, minutesAway: Int) async {
let updatedState = DeliveryAttributes.ContentState(driverName: "Alex", minutesAway: minutesAway)
await activity.update(.init(state: updatedState, staleDate: nil))
}
#Why it matters
- Updates push live content to the Lock Screen and Dynamic Island without the user reopening the app
Activity.requestand.updatework locally from the app, or remotely via push updates for state changes that originate server-side- One
ActivityAttributes.ContentStatetype drives both the Lock Screen banner and the compact/expanded Dynamic Island layouts, defined together in a widget extension'sActivityConfiguration - Removes the need for local-notification hacks or background polling just to keep a status visible
#Gotcha
Live Activities have a hard duration limit — they end automatically after several hours of inactivity regardless of whether you ever call end(), so a long-running process needs its own way to notify the user once the activity has been force-ended. ContentState also has to stay small and Codable; a payload that pushes past the system's size limit for a push update fails silently rather than throwing where you'd expect. Verify current duration and payload limits against Apple's docs, since both have shifted across iOS releases since the 16.1 launch.