Sitemap

Live Activities in iOS: A SwiftUI Starter Guide for Dynamic Island

5 min readJan 20, 2026

--

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

Press enter or click to view image in full size

2.Search for Widget Extension in the filter at right top and click next

Press enter or click to view image in full size

3.Select the minimal config as show in below screenshot

Press enter or click to view image in full size

4.Once done, you will see a new target added to your project

Step 2: Update the info.plist file of your App

  1. Navigate to project settings, select your app target and go to the info tab
  2. Now add NSSupportsLiveActivities key in that and select YES as value
Press enter or click to view image in full size

Step 3: Implementation

  1. 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

Press enter or click to view image in full size

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.

Press enter or click to view image in full size

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.

Press enter or click to view image in full size
Press enter or click to view image in full size
Press enter or click to view image in full size

Also if you remove style: .transient, while starting the live activity, you can see below when phone is locked

Press enter or click to view image in full size

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.

--

--