ActivityKit (iOS 16.1) shows an ongoing activity on the lock screen and in the Dynamic Island. The activity is a WidgetKit widget without a timeline provider: the app starts, updates, and ends it.
Know the Limits First
The system ends a live activity after eight hours, so it fits matches and deliveries, not long flights. The dynamic state must stay under 4 KB. The app must be in the foreground to start one, and the app target's Info.plist needs the Supports Live Activities key set to YES.
Split the Data Into Static and Dynamic
Conform to ActivityAttributes. The attributes hold what never changes; the nested ContentState holds what updates.
import ActivityKit
struct FootballMatch: ActivityAttributes {
typealias ContentState = Score
struct Score: Codable, Hashable {
let score1: Int
let score2: Int
}
let team1: String
let team2: String
}
Start, Update, End
Activity.request returns a handle. Keep it, then push new states or end with a dismissal policy. BackgroundTasks or push notifications can drive updates while the app is not in the foreground.
let activity = try Activity.request(
attributes: FootballMatch(team1: "AJAX", team2: "PSV"),
contentState: .init(score1: 0, score2: 0)
)
Task { await activity.update(using: .init(score1: 1, score2: 0)) }
Task {
await activity.end(
using: .init(score1: 1, score2: 0),
dismissalPolicy: .immediate
)
}
Present With ActivityConfiguration
The widget extension defines one ActivityConfiguration per attributes type: a lock screen view plus the four Dynamic Island slots (expanded regions, compact leading, compact trailing, minimal).
@available(iOSApplicationExtension 16.1, *)
struct FastingActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: FastingAttributes.self) { context in
LiveActivityView(context: context)
.padding(.horizontal)
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.center) {
LiveActivityView(context: context)
}
} compactLeading: {
Image(systemName: "circle").foregroundColor(.green)
} compactTrailing: {
Text(verbatim: context.state.progress.formatted(.percent))
} minimal: {
Image(systemName: "circle").foregroundColor(.green)
}
}
}
}
Add it to the widget bundle behind an availability check, next to the regular widgets.
@main struct FastBotWidgetBundle: WidgetBundle {
var body: some Widget {
FastBotWidget()
if #available(iOS 16.1, *) {
FastingActivityWidget()
}
}
}