This is the first project example referring to the latest Apple ActivityKit beta and Dynamic Island (NEW) release.
Live Activities will help you follow an ongoing activity right from your Lock Screen, so you can track the progress of your food delivery or use the Now Playing controls without unlocking your device.
Your app’s Live Activities display on the Lock Screen and in Dynamic Island — a new design that introduces an intuitive, delightful way to experience iPhone 14 Pro and iPhone 14 Pro Max.
- iOS 16.1 or above
- Xcode 14.1 or above
Dynamic Island: https://1998design.medium.com/how-to-create-dynamic-island-widgets-on-ios-16-1-or-above-dca0a7dd1483
Live Activities: https://1998design.medium.com/how-to-create-live-activities-widget-for-ios-16-2c07889f1235
Add NSSupportsLiveActivities key and set to YES.
import ActivityKitstruct PizzaDeliveryAttributes: ActivityAttributes {
public typealias PizzaDeliveryStatus = ContentState
public struct ContentState: Codable, Hashable {
var driverName: String
// Changed from Date to ClosedRange<Date> - 16.1
var estimatedDeliveryTime: ClosedRange<Date>
}
var numberOfPizzas: Int
var totalAmount: String
}func startDeliveryPizza() {
let pizzaDeliveryAttributes = PizzaDeliveryAttributes(numberOfPizzas: 1, totalAmount:"$99")
// Date() changed to Date()...Date() - 16.1
let initialContentState = PizzaDeliveryAttributes.PizzaDeliveryStatus(driverName: "TIM 👨🏻🍳", estimatedDeliveryTime: Date()...Date().addingTimeInterval(15 * 60))
do {
let deliveryActivity = try Activity<PizzaDeliveryAttributes>.request(
attributes: pizzaDeliveryAttributes,
contentState: initialContentState,
pushType: nil)
print("Requested a pizza delivery Live Activity \(deliveryActivity.id)")
} catch (let error) {
print("Error requesting pizza delivery Live Activity \(error.localizedDescription)")
}
}
func updateDeliveryPizza() {
Task {
// Date() changed to Date()...Date() - 16.1
let updatedDeliveryStatus = PizzaDeliveryAttributes.PizzaDeliveryStatus(driverName: "TIM 👨🏻🍳", estimatedDeliveryTime: Date()...Date().addingTimeInterval(60 * 60))
for activity in Activity<PizzaDeliveryAttributes>.activities{
await activity.update(using: updatedDeliveryStatus)
}
}
}
func stopDeliveryPizza() {
Task {
for activity in Activity<PizzaDeliveryAttributes>.activities{
await activity.end(dismissalPolicy: .immediate)
}
}
}
func showAllDeliveries() {
Task {
for activity in Activity<PizzaDeliveryAttributes>.activities {
print("Pizza delivery details: \(activity.id) -> \(activity.attributes)")
}
}
}import ActivityKit
import WidgetKit
import SwiftUI
@main
struct Widgets: WidgetBundle {
var body: some Widget {
PizzaDeliveryActivityWidget()
}
}
struct PizzaDeliveryActivityWidget: Widget {
var body: some WidgetConfiguration {
// attributesType changed to for - 16.1
ActivityConfiguration(for: PizzaDeliveryAttributes.self) { context in
VStack(alignment: .leading) {
HStack {
VStack(alignment: .leading) {
Text("\(context.state.driverName) is on the way!").font(.headline)
HStack {
VStack {
Divider().frame(height: 6).overlay(.blue).cornerRadius(5)
}
Image(systemName: "box.truck.badge.clock.fill").foregroundColor(.blue)
VStack {
RoundedRectangle(cornerRadius: 5)
.stroke(.secondary, style: StrokeStyle(lineWidth: 1, dash: [5]))
.frame(height: 6)
}
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
VStack {
RoundedRectangle(cornerRadius: 5)
.stroke(.secondary, style: StrokeStyle(lineWidth: 1, dash: [5]))
.frame(height: 6)
}
Image(systemName: "house.fill").foregroundColor(.green)
}
}.padding(.trailing, 25)
Text("\(context.attributes.numberOfPizzas) 🍕").font(.title).bold()
}.padding(5)
Text("You've already paid: \(context.attributes.totalAmount) + $9.9 Delivery Fee 💸").font(.caption).foregroundColor(.secondary)
}.padding(15)
}
// NEW 16.1
dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Label("\(context.attributes.numberOfPizzas) Pizzas", systemImage: "bag")
.font(.title2)
}
DynamicIslandExpandedRegion(.trailing) {
Label {
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
.multilineTextAlignment(.trailing)
.frame(width: 50)
.monospacedDigit()
.font(.caption2)
} icon: {
Image(systemName: "timer")
}
.font(.title2)
}
DynamicIslandExpandedRegion(.center) {
Text("\(context.state.driverName) is on his way!")
.lineLimit(1)
.font(.caption)
}
DynamicIslandExpandedRegion(.bottom) {
Button {
// Deep link into the app.
} label: {
Label("Contact driver", systemImage: "phone")
}
}
} compactLeading: {
Label {
Text("\(context.attributes.numberOfPizzas) Pizzas")
} icon: {
Image(systemName: "bag")
}
.font(.caption2)
} compactTrailing: {
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
.multilineTextAlignment(.center)
.frame(width: 40)
.font(.caption2)
} minimal: {
VStack(alignment: .center) {
Image(systemName: "timer")
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
.multilineTextAlignment(.center)
.monospacedDigit()
.font(.caption2)
}
}
.keylineTint(.accentColor)
}
}
@available(iOSApplicationExtension 16.2, *)
struct PizzaDeliveryActivityWidget_Previews: PreviewProvider {
static let activityAttributes = PizzaDeliveryAttributes(numberOfPizzas: 2, totalAmount: "1000")
static let activityState = PizzaDeliveryAttributes.ContentState(driverName: "Tim", estimatedDeliveryTime: Date()...Date().addingTimeInterval(15 * 60))
static var previews: some View {
activityAttributes
.previewContext(activityState, viewKind: .content)
.previewDisplayName("Notification")
activityAttributes
.previewContext(activityState, viewKind: .dynamicIsland(.compact))
.previewDisplayName("Compact")
activityAttributes
.previewContext(activityState, viewKind: .dynamicIsland(.expanded))
.previewDisplayName("Expanded")
activityAttributes
.previewContext(activityState, viewKind: .dynamicIsland(.minimal))
.previewDisplayName("Minimal")
}
}Live Activity widgets on the Lock Screen are snapshot-rendered with a strict render budget. Two side effects matter here:
TimelineView(.explicit([...]))/.periodic(...)inside the content closure is unreliable — iOS throttles / drops scheduled snapshots once the phone is locked or the app is backgrounded.Text(timerInterval:)andProgressView(timerInterval:)with.linearare the only primitives iOS interpolates frame-to-frame; everything else is a static snapshot until the nextactivity.update()/ push.staleDatealone cannot flip the widget to its "done" layout at an exact time offline. Without a real push, the snapshot taken aroundstaleDatecan lag by multiple seconds.
To flip a Live Activity to its end state offline, at the exact time, with no APNs push, this project ships two app-side keep-alive strategies that keep the app runnable in the background long enough to fire activity.update(finalContent) precisely at endDate:
| Strategy | Mechanism | Natural fit |
|---|---|---|
LocationKeepAlive |
CLLocationManager.startMonitoringSignificantLocationChanges() |
Delivery / navigation apps |
AudioKeepAlive |
Silent PCM WAV looped on AVAudioSession(.playback, .mixWithOthers) |
Timer / meditation / workout apps |
Both run a DispatchSourceTimer to endDate, then call activity.update(finalContent). The UI toggle (Location [ ⇆ ] Sound) in ContentView picks which side-channel starts for each delivery.
Required capabilities (both added to Info.plist):
<key>UIBackgroundModes</key>
<array>
<string>location</string>
<string>audio</string>
</array>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Track your driver in the background so the Live Activity stays fresh.</string>Because TimelineView is unreliable on the lock screen, any discrete in-progress state change (e.g. warehouse icon → ✓ when the fill sweeps past the midpoint marker) also needs a keep-alive-driven push. The same DispatchSourceTimer pattern fires a second time at startDate + duration/2, re-pushing the current state just to force a snapshot — the widget body then re-evaluates Date() >= midpoint and flips the icon:
LocationKeepAlive.shared.start(
until: endDate,
midpoint: Date().addingTimeInterval(duration / 2),
midpointFire: pushSnapshot, // flips warehouse → ✓
fire: pushSnapshot // flips bar → delivered layout
)The lock-screen notification bar renders a 3-stop delivery journey:
[📦 package]──[⏱ timer 🏢 warehouse]──[🏠 home]
- Track: full-width
Capsule()in gray. - Fill:
ProgressView(timerInterval:)with.linearstyle and aLinearGradient(colors: [.blue, .green])tint — the only way to get a frame-by-frame smooth fill inside a Live Activity.scaleEffect(x: 1, y: 7)fattens the ~4pt system bar to 28pt;clipShape(Capsule())keeps the leading edge vertical. - Icons: package (left), warehouse (center, dead-centered via a
HStack(spacing: 0)split into twomaxWidth: .infinityhalves with the warehouse icon as the seam), home (right). Timer text sits on the left of the warehouse inside the left half. - Midpoint flip: warehouse 🏢 → ✓ via the midpoint keep-alive push.
- End state: the same bar, fully filled
LinearGradient, with a single ✓ parked at the right — reached via the final keep-alive push atendDate. Bottom paid-footer text stays so the widget doesn't change height between in-progress / delivered.
Console: Requested a pizza delivery Live Activity DA288E1B-F6F5-4BF1-AA73-E43E0CC13150Updating content state for activity DA288E1B-F6F5-4BF1-AA73-E43E0CC13150Console: Pizza delivery details: DA288E1B-F6F5-4BF1-AA73-E43E0CC13150 -> PizzaDeliveryAttributes(numberOfPizzas: 1, totalAmount: "$99")Q1. Can I use Local Assets Folder?
A1. YES.
✅ Easy to implement
✅ May possible to change image (string name) when updating the event
❎ Limited options and big app size.
If you need to add more image sets, then re-upload to App Store is required (Time wasting, and not all users can get the instant update)
Q2. Can I use Network Image?
A2. YES. Load the image from the Internet, and pass the data to the widget via App Group and AppStorage (aka UserDefaults)
✅ Update in any time as the url can be changed / modify remotely.
✅ No need to store in Assets Folder and reduced app size.
❎ Unless the user re-open the app, the image cannot be updated in the background.
Q3. How about AsyncImage?
A3. NO. (Known not working)
Both cases 1 & 2 are already demoed in the sample project.
Swift® and SwiftUI® are trademarks of Apple Inc.


