Live Activities in iOS: A SwiftUI Starter Guide for Dynamic Island
Rather than covering the full Live Activities API, this article focuses on the smallest possible implementation to get something visible in the Dynamic Island using SwiftUI. It intentionally doesn’t walk through every line of code, with the goal of helping developers get started quickly and explore the rest of the system at their own pace.
Step 1: Create a new target for widget
1.Go to File -> New -> Target
2.Search for Widget Extension in the filter at right top and click next
3.Select the minimal config as show in below screenshot
4.Once done, you will see a new target added to your project
Step 2: Update the info.plist file of your App
- Navigate to project settings, select your app target and go to the info tab
- Now add NSSupportsLiveActivities key in that and select YES as value
Step 3: Implementation
- While creating target since I named it as TestWidget, I will see following files in my new target. I will refer the same files from now on
a.TestWidget.swift
b.TestWidgetBundle.swift
c.TestWidgetLiveActivity.swift
2. In your main app, you need to create the Attribute file, where you will declare the variable values that will be passed to your live activity
import ActivityKit
struct TestWidgetAttributes: ActivityAttributes {
struct ContentState: Codable, Hashable {
var progress: Double
//var label: String
}
}Here you can add more than one variable if needed like label, since i am using only one variable i have commented out label
3. Since this file is in app target and we will need to refer it in our widget target too, we have to give its reference to widget target as well
a. Select the TestWidgetAttribute.swift file and go to the target membership tab, you can find it by clicking on the right hand top inspector tab
b. Now click on the plus icon in the target membership and add your widget and click save. Now you can refer to the attribute file in the widget too.
4. Now let’s update the code from test widget module
Get Rutwij’s stories in your inbox
Join Medium for free to get updates from this writer.
a. TestWidget.swift
import WidgetKit
import SwiftUI
struct TestWidgetView: View {
let progress: Double
var body: some View {
ZStack {
Text("\(Int(progress * 10))")
.font(.headline)
.monospacedDigit()
ProgressView(value: progress)
.progressViewStyle(.circular)
.frame(height: 25)
}
}
}b. TestWidgetLiveActivity.swift
import ActivityKit
import WidgetKit
import SwiftUI
struct TestWidgetLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: TestWidgetAttributes.self) { _ in
Text("Test")
} dynamicIsland: { context in
let progress = context.state.progress
return DynamicIsland {
DynamicIslandExpandedRegion(.center) {
TestWidgetView(
progress: progress
)
}
} compactLeading: {
Text("Test")
.padding(.leading, 10)
} compactTrailing: {
TestWidgetView(
progress: progress
)
} minimal: {
Text("\(progress)")
}
}
}
}c. TestWidgetBundle.swift
import WidgetKit
import SwiftUI
@main
struct TestWidgetBundle: WidgetBundle {
var body: some Widget {
TestWidgetLiveActivity()
}
}5. Now let’s move into the main app and finish the implementation
a. Start live activity
Note: pushType: nil & style: .transient, can be used from iOS 18 and above
If you are targeting below that remove those. Or add conditionally as per your requirement
style: .transient — will kill your live activity when phone is locked or app is killed, if you don’t add it your live activity will persists even if you kill the app or phone is locked and you have to manually call end api
func startLiveActivity(progress: Double) {
Task {
do {
let activity = try Activity.request(
attributes: TestWidgetAttributes(),
content: .init(
state: .init(
progress: progress
),
staleDate: nil
),
pushType: nil,
style: .transient
)
print("Started:", activity.id)
} catch {
print("Error:", error)
}
}
}b. Update live activity
func updateLiveActivityScore(_ progress: Double) {
Task {
for activity in Activity<TestWidgetAttributes>.activities {
await activity.update(
.init(
state: .init(progress: progress),
staleDate: .now.addingTimeInterval(3),
relevanceScore: 0.01
)
)
}
}
}c. End live activity
func endLiveActivity() {
Task {
let activities = Activity<TestWidgetAttributes>.activities
print("Active activities:", activities.count)
for activity in activities {
print("Ending activity:", activity.id)
await activity.end(
.init(
state: activity.content.state,
staleDate: nil
),
dismissalPolicy: .immediate
)
}
}
}App Code(UI + Actions)
import SwiftUI
import ActivityKit
struct ContentView: View {
@State var progress: Double = 0
var body: some View {
VStack {
Button("Start Live Activity"){
startLiveActivity(progress: progress)
}.padding(.vertical, 20)
Button("Update Progress"){
progress+=1.0
updateLiveActivityScore(progress)
}.padding(.vertical, 20)
Button("End Live Activity"){
endLiveActivity()
}.padding(.vertical, 20)
}
.padding()
}
}
func startLiveActivity(progress: Double) {
Task {
do {
let activity = try Activity.request(
attributes: TestWidgetAttributes(),
content: .init(
state: .init(
progress: progress
),
staleDate: nil
),
pushType: nil,
style: .transient
)
print("Started:", activity.id)
} catch {
print("Error:", error)
}
}
}
func updateLiveActivityScore(_ progress: Double) {
Task {
for activity in Activity<TestWidgetAttributes>.activities {
await activity.update(
.init(
state: .init(progress: progress),
staleDate: nil
)
)
}
}
}
func endLiveActivity() {
Task {
let activities = Activity<TestWidgetAttributes>.activities
print("Active activities:", activities.count)
for activity in activities {
print("Ending activity:", activity.id)
await activity.end(
.init(
state: activity.content.state,
staleDate: nil
),
dismissalPolicy: .immediate
)
}
}
}After completing these steps, you should see results similar to the following.
Also if you remove style: .transient, while starting the live activity, you can see below when phone is locked
With this basic implementation in place, you should now have a solid starting point for experimenting with Live Activities and the Dynamic Island. As you explore more complex scenarios, Apple’s official documentation provides detailed guidance on everything from lifecycle management to advanced UI customisation.





