Streaming is available in most browsers,
and in the Developer app.
-
Meet ActivityKit
Live Activities are a glanceable way for someone to keep track of the progress of a task within your app. We'll teach you how you can create helpful experiences for the Lock Screen, the Dynamic Island, and StandBy. Learn how to update your app's Live Activities, monitor activity state, and take advantage of WidgetKit and SwiftUI to build richer experiences.
Chapters
- 0:00 - Intro
- 0:36 - Live Activity overview
- 4:23 - Lifecycle of Live Activities
- 10:43 - Building Live Activity UI
- 16:37 - Wrap-up
Resources
- ActivityKit
- Displaying live data with Live Activities
- Human Interface Guidelines: Live Activities
- Starting and updating Live Activities with ActivityKit push notifications
- WidgetKit
Related Videos
WWDC23
-
Download
♪ ♪ Can: Hi there, I’m Can Aran, and I’m an engineer on the iOS System Experience team.
It’s my pleasure to talk to you about Live Activities.
In this talk, I’ll be giving you an overview of Live Activities.
Then I’ll go through the lifecycle of live activities. Finally, I’ll show you how to build an immersive and simple UI for your activity.
To start, I’ll dig into what Live Activities can do.
Live Activities are an immersive, glanceable way to keep track of an event or the progress of a task.
Live Activities have a discrete start and end, and can provide real-time updates from background app runtime or remotely using Push Notifications.
Here are great examples from United Airlines and MLB.
Live Activities are even more immersive on iPhone 14 Pro and Pro Max.
The Dynamic Island displays Live Activities throughout the system when your app is in the background.
When one Live Activity is active, it’s rendered using its variable-width, “compact” presentation.
The Dynamic Island displays up to two live activities at a time.
One of these Live Activities appears attached to the TrueDepth camera, while the other renders in its own detached view.
Both of these Live Activities use their “minimal” presentation.
At any time, a person can long press a Live Activity to display its “expanded” presentation, giving them even more glanceable information.
In the expanded presentation, views can deep link to different areas within your app, providing a rich user experience. There are some new experiences for Live Activities in iOS 17. In addition to the Lock screen and Dynamic Island, Live Activities appear in StandBy. And now, iPad also supports Live Activities.
Enable your implementation on iPadOS, and bring your immersive Live Activities to iPad, like this one from Crumbl Cookies.
With iOS 17, you can add interactivity to your Live Activities using WidgetKit and SwiftUI. You can add buttons or toggles to enhance the user experience. Learn more about how to bring interactivity to your widgets in Luca’s video, “Bring Widgets to life." Live Activities rely on the ActivityKit framework, empowering your app to request, update, and manage their lifecycles.
They are laid out declaratively using SwiftUI and WidgetKit.
If you’ve implemented a Home Screen widget before, this will feel very familiar.
A Live Activity can be requested when your app is in the foreground.
Your app should only request a Live Activity after discrete user action, possibly “following” an event, or explicitly beginning a task.
This is crucial to ensure a positive user experience.
Live Activities are user-moderated similar to Notifications.
Someone can easily dismiss or turn them off for your app altogether.
The API requires you to support all presentations from the Lock Screen to all three Dynamic Island presentations.
In StandBy, the system scales your Lock Screen presentation to fill the screen.
In addition to relying on background runtime, your app can update Live Activities remotely by using push notifications with the “liveactivity” push type.
For more information about how to update your Live Activities with push notifications, you can check out Jeff’s video. Your app’s Live Activity goes through different phases during its lifecycle.
I’m building a Live Activity where a person can choose a hero from the Emoji Rangers app and take them on an adventure.
During the adventure, the hero will face challenges and fight with bosses. I’ll display important moments of this adventure in my Live Activity.
This live activity displays the most essential information about a hero’s adventure. It includes the hero’s name and statistics, their avatar, health level, and a description about what this hero experiences on their adventure. The Lifecycle of a Live Activity contains four main steps. Start by requesting an activity. Once it has started, update it with your latest content. In the meantime, observe your activity to react to state changes, such as people ending it.
When the task is completed, make sure to end the activity. Requesting a Live Activity is very straightforward.
Make sure your app is in the foreground, and configure your app so that you have an initial content and necessary activity request data. Before I can request a Live Activity in the Emoji Rangers app, I have to start by defining a set of static and dynamic data for my Live Activity by implementing "ActivityAttributes." I call it "AdventureAttributes." "AdventureAttributes" describes one static data, which is the hero.
It also defines the required custom "ContentState" which encapsulates the hero's health level and the event description.
As these properties change, my Live Activity UI will get updated and I’ll be able to show the current state of the adventure on the screen.
Now that the dynamic and static data is ready, I’ll set up the adventure activity requests.
I’ll start creating an instance of AdventureAttributes with the hero and set up the initial content with the hero’s health level and an event description.
Each activity content can be provided with a stale date to inform the system when the content is considered out-of-date. For now, I’ll pass in nil.
Relevance score for the content determines the order in which each Live Activity appears when several adventure activities are started. If I were going to start another adventure activity, I would specify different relevance scores for each.
Passing a relevance score is optional.
The default value is zero.
I can now request the activity.
I’ll pass in the attributes, initial content, and the push notification type.
Push notification type indicates if the Live Activity receives updates to its dynamic content with ActivityKit push notifications.
For this example, I’ll set it to "nil," which means this activity can only receive updates locally. In order to begin this Live Activity, the Live Activity setting for the Emoji Rangers app needs to be enabled. Now that I can request my Live Activity, I’ll look into how I can update the adventure when my hero goes through thrilling tasks. The dynamic attributes tell me when to update my Live Activity.
Whenever the event description or hero’s health level changes, I’ll update my activity.
Oh no! The hero takes a critical hit from a boss.
So I create a "contentState" that reflects the change in the health level and describes the events. Since hero’s health level reduced significantly, I need to send an alert.
I’ll create an alert configuration for that.
This will display an alert on iPhone, iPad, or on a synced Apple Watch if some significant information changes with the Live Activity.
In this case, the hero is injured badly and needs a potion to heal.
The configuration title and body are only used on Apple Watch, and displayed as a notification.
On iPhone and on iPad, the activity UI with the updated content appears with the specified sound.
Now I can call the update API on the activity object with the updated content and the alert configuration.
This will make sure that the Live Activity UI is updated and the user is alerted with this update.
Activity state changes can happen any time during the lifecycle of a live activity.
There are 4 possible states: "started," "finished," "dismissed" and "stale." I observe these states using the activityStateUpdates API on the activity object to receive the updates asynchronously.
When the activity gets dismissed, I make sure that I’m not keeping track of the adventure data anymore, and update the UI in the app so that I don’t show an ongoing activity.
I can also check the state through activityState API to retrieve it synchronously when needed.
My hero went through a lot.
It’s now time to end the adventure Live Activity.
To be able to end the activity, I start by creating a final content.
My content will show the final state of the adventure where the hero defeats the boss.
Then I’ll determine a dismissal policy for my UI. The default policy is suitable for this case. This policy ensures that the adventure information appears on the Lock Screen for some time after it ends so that someone can glance at the Lock Screen to see what happened at the end of the adventure.
I can now end the adventure activity and give the hero a rest.
I’ve built all the logic around my Live Activity lifecycle.
It’s time to focus on the activity UI.
Emoji Ranger widget extension currently has two widgets in its WidgetBundle.
I need to add the Live Activity configuration in the WidgetBundle.
I’ll call it "AdventureActivityConfiguration." "AdventureActivityConfiguration" leverages widget infrastructure, and it needs to return a WidgetConfiguration in its body.
I’ll create an ActivityConfiguration object, which describes the content of my Live Activity.
For each presentation closure, ActivityConfiguration object provides an ActivityViewContext which stores my static and dynamic attributes and the activity ID.
This context is created based on the attributes type passed into the configuration.
This type must match with the attribute that your activity is requested with.
I’ll pass the "AdventureAttributes" type so that the activity configuration can be initialized successfully.
The first closure in "ActivityConfiguration" specifies the Lock Screen UI.
As my view context changes with the activity updates, this UI will be rendered for each update.
Similar to widgets, I don’t provide the size of the Lock Screen UI for my Live Activity, but let the system determine the appropriate dimensions.
For the Emoji Ranger activity, I’ll show the hero information, name and avatar, health level, and event description on the Lock Screen with a navy blue background.
"AdventureLiveActivityView" will have all that information through the passed view context. My Live Activity on the Lock Screen looks simple, elegant, and it has all the information that I need about what the hero is going through in the adventure.
Now that I wrapped up my Lock Screen UI, I need to implement my Dynamic Island presentations.
There are three presentations: compact, minimal and expanded.
When my app’s Live Activity is the only one running on the system, it’ll be displayed using the compact presentation.
The compact presentation has two areas, leading and trailing. They appear together to form a cohesive presentation in the Dynamic Island.
Choose essential content to show in the leading and trailing space, since the space is limited.
Users should be able to identify the specific activity by looking at the content here.
In the “DynamicIsland" closure of the ActivityConfiguration object, I again have access to the view context to create my expanded, compactLeading, compactTrailing, and minimal views.
I need to create a DynamicIsland view builder to represent each of those presentations. For my hero’s adventure, I’ll add the hero avatar to leading content and the health level to the trailing view. I’ll also have a dynamic tint color based on my hero’s health level.
The compact presentation for the adventure is ready now.
When more than one app starts a Live Activity, the system chooses which Live Activities are visible and displays both of them using the minimal presentation for each: one minimal presentation appears attached to the Dynamic Island while the other appears detached.
Your minimal view should only have the most critical information, as you have very limited space to work with.
For the minimal view in my Live Activity, the most important information to show is who the hero is and hero’s health, so I’ll show the avatar and the health level with a dynamic tint color.
This way, users will know when to help out their hero by looking at the minimal view. When users touch and hold a Live Activity in a compact or minimal presentation, the system displays the content in an expanded presentation. I need to support that too.
For the expanded presentation, the system divides the expanded presentation into different areas. The first closure of the DynamicIsland view builder represents the expanded content. Within that closure, each section content can be defined with the expanded region passing the specific position.
I’ll add the hero name and the avatar to the leading space, hero statistics to the trailing space, and at last, the health bar and the event description into the bottom space.
In the end, my Dynamic Island UI looks simple and provides all the necessary information for the adventure.
Now I’m ready to go on adventures with my favorite heroes, and follow along with the simple, yet immersive Live Activity UI that I just created. While designing your own UI, display only the most essential content in the Live Activity.
Keep it simple and show additional details on your app when the user taps on the Live Activity. Check out “Design dynamic Live Activities” for more information.
Use Live Activities as a powerful tool to show your glanceable and live information of an ongoing activity. With its simple configuration, create a dynamic way to engage with your users on iOS and iPadOS.
To learn more about pushing updates, check out “Update Live Activities with push notifications.” I can't wait to see what you'll build with ActivityKit.
Thank you for watching! ♪ ♪
-
-
5:40 - Define ActivityAttributes
import ActivityKit struct AdventureAttributes: ActivityAttributes { let hero: EmojiRanger struct ContentState: Codable & Hashable { let currentHealthLevel: Double let eventDescription: String } }
-
6:28 - Request Live Activity with initial content state
let adventure = AdventureAttributes(hero: hero) let initialState = AdventureAttributes.ContentState( currentHealthLevel: hero.healthLevel, eventDescription: "Adventure has begun!" ) let content = ActivityContent(state: initialState, staleDate: nil, relevanceScore: 0.0) let activity = try Activity.request( attributes: adventure, content: content, pushType: nil )
-
8:00 - Update Live Activity with new content
let heroName = activity.attributes.hero.name let contentState = AdventureAttributes.ContentState( currentHealthLevel: hero.healthLevel, eventDescription: "\(heroName) has taken a critical hit!" ) var alertConfig = AlertConfiguration( title: "\(heroName) has taken a critical hit!", body: "Open the app and use a potion to heal \(heroName)", sound: .default ) activity.update( ActivityContent<AdventureAttributes.ContentState>( state: contentState, staleDate: nil ), alertConfiguration: alertConfig )
-
9:30 - Observe activity state
// Observe activity state asynchronously func observeActivity(activity: Activity<AdventureAttributes>) { Task { for await activityState in activity.activityStateUpdates { if activityState == .dismissed { self.cleanUpDismissedActivity() } } } } // Observe activity state synchronously let activityState = activity.activityState if activityState == .dismissed { self.cleanUpDismissedActivity() }
-
10:03 - Dismiss Live Activity with final content state
let hero = activity.attributes.hero let finalContent = AdventureAttributes.ContentState( currentHealthLevel: hero.healthLevel, eventDescription: "Adventure over! \(hero.name) has defeated the boss! Congrats!" ) let dismissalPolicy: ActivityUIDismissalPolicy = .default activity.end( ActivityContent(state: finalContent, staleDate: nil), dismissalPolicy: dismissalPolicy) }
-
10:50 - Add ActivityConfiguration to WidgetBundle
import WidgetKit import SwiftUI @main struct EmojiRangersWidgetBundle: WidgetBundle { var body: some Widget { EmojiRangerWidget() LeaderboardWidget() AdventureActivityConfiguration() } }
-
11:05 - Define Lock Screen presentation
struct AdventureActivityConfiguration: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: AdventureAttributes.self) { context in AdventureLiveActivityView( hero: context.attributes.hero, isStale: context.isStale, contentState: context.state ) .activityBackgroundTint(Color.navyBlue) } dynamicIsland: { context in // ... } } }
-
13:28 - Define Dynamic Island compact presentation
struct AdventureActivityConfiguration: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: AdventureAttributes.self) { context in // ... } dynamicIsland: { context in DynamicIsland { // ... } compactLeading: { Avatar(hero: context.attributes.hero) } compactTrailing: { ProgressView(value: context.state.currentHealthLevel) { Text("\(Int(context.state.currentHealthLevel * 100))") } .progressViewStyle(.circular) .tint(context.state.currentHealthLevel <= 0.2 ? Color.red : Color.green) } minimal: { // ... } } } }
-
14:42 - Define Dynamic Island minimal presentation
struct AdventureActivityConfiguration: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: AdventureAttributes.self) { context in // ... } dynamicIsland: { context in DynamicIsland { // ... } compactLeading: { // ... } compactTrailing: { // ... } minimal: { ProgressView(value: context.state.currentHealthLevel) { Avatar(hero: context.attributes.hero) } .progressViewStyle(.circular) .tint(context.state.currentHealthLevel <= 0.2 ? Color.red : Color.green) } } } }
-
15:26 - Define Dynamic Island expanded presentation
struct AdventureActivityConfiguration: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: AdventureAttributes.self) { context in // ... } dynamicIsland: { context in DynamicIsland { // Leading region DynamicIslandExpandedRegion(.leading) { LiveActivityAvatarView(hero: hero) } // Expanded region DynamicIslandExpandedRegion(.trailing) { StatsView(hero: hero, isStale: isStale) } // Bottom region DynamicIslandExpandedRegion(.bottom) { HealthBar(currentHealthLevel: contentState.currentHealthLevel) EventDescriptionView(hero: hero, contentState: contentState) } } compactLeading: { // ... } compactTrailing: { // ... } minimal: { // ... } } } }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.