Extend the reach of your iOS application with Live Activities

Hi, I'm Arnaud @ArnaudDerosin. I am working as a Mobile Engineer at Nature and for this 16th day of the Nature Engineering Blog Festival (July 2023) my topic is about iOS Live Activities.

First, I decided to request this useful friend to get my answer but it seems like he has not done his lessons since few years ago so let me explain. 😄

In my previous published articles, I introduced how to extend the reach of your app adding widget for your Android / iOS app and with Lock screen widgets on iOS. To extend even more the reach of your app(s) and allow users to get necessary information at a glance, today I would like to discuss about Live activities introduced with iOS16.

At the time I’m righting this article this feature is not supported in macOS, tvOS, visionOS, or watchOS but could be an extremely useful one for your iOS, iPadOS users!

Let me guide you through this article! 🎇

Introduction

A Live Activity displays up-to-date information from your app, allowing people to view the progress of events or tasks at a glance.

One of the goal of the Live activities is to improve the user experience that is quite limited by the notifications capability, create a new system that lets you follow ongoing activity in a more powerful and intuitive way.

Sports, delivery tracking are examples of usage where showing Live activity would be useful for the user (where you want to track for a few minutes to couple hours an activity).

Providing useful functionality is primordial and since it will be visible on the OS screen level, consider to learn about the design guidelines that allow you to not break OS logics and smoothly integrate, it will play an important role in the user decision to use it or not on the long term.

Where live activities can be presented?

  • Lockscreen

Live Activities will be visible at the top of the list (above notifications)

Live Activity position on LockScreen

  • Dynamic island

A lot of information are really important to understand to make this looking great in the Dynamic Island space. I recommend you to correctly understand the importance of keeping design concentric, knowing different views and their specific usages, balance, shrinking units to integrate it well. (Guidelines and WWDC talk providing awesome content)

Live Activity position with Dynamic Island (to illustrate)

Personally, I used the Dynamic Island feature for almost 1 year now and I find it really useful, it opened a new way to multitask and monitor activities (timer, map, shared connection) allowing us to keeping eye on what’s happening in the background and to interact very quickly, use it at your advantage. 😉

  • StandBy mode (available from iOS 17)

It will fill the screen

When a person taps the minimal presentation in StandBy, the system scales the Lock Screen appearance by 2x to fill the screen so make sure your assets look great in the scaled-up presentation.

Source: Apple Live Activities Human Interface Guidelines

I will not focus my explanation much about this upcoming feature in this article but related to the information you want to display consider removing your background when in standby mode to look seamlessly integrated this devices bezels for a soften ambient look.

Also in low light environment your live activity color will be atomically transitioned to red light colors, check the contrast to be sure it’s usable in that environment.

How is it implemented?

I will show you an easy steps by steps guide on how to support live activity for the LockScreen / Dynamic Island as I think it will simplify understanding and that you will be free to let go of your creativity to create awesome Live Activities for your specific use case that at the end of this example.

Note: Since the Live Activities have been released, multiples methods have been deprecated, this article is updated and I hope will allow you to get started right. As always, if anything differs please check the documentation!

Environment

1 - Initial set up

Create a new Xcode project if you don’t have one

Add a widget extension to your project

  • File > New > Target…
  • Search for Widget Extension
  • Add the name you want for the widget (named WidgetExample here)
  • Make sure Include Live Activity is selected when you add a widget extension target to your Xcode project. (I deselected Include configuration Intent since I will not cover this part in this example. Feel free to add if you want to support configuration)
  • Click the Finish button.

Import ActivityKit to the WidgetExample.swift file

2 - Add LiveActivity permission to the app

Select your project then click the Info tab showing your Info.plist

Click the + anywhere on the list and type NSSupportsLiveActivities. It will show you a boolean value by default set to NO, change it to YES

That’s it, you should be able to successfully build your project

Info.plist with NSSupportsLiveActivities

3- Add code that defines an ActivityAttributes structure to describe the static and dynamic data of your Live Activity.

Bellow, I created a new struct that conform to the ActivityAttributes protocol and describes the content that I want to appear in my Live Activity.

//
//  EcoTimerAttributes.swift
//  LiveActivityExample
//
//  Created by Arnaud Derosin on 2023/07/22.
//

import Foundation
import ActivityKit

struct EcoTimerAttributes: ActivityAttributes {
    // Dynamic data
    public typealias EcoTimerStatus = ContentState
    
    public struct ContentState: Codable, Hashable {
        var ecoTimerTemperatureAdjust: String
        var ecoTimerEndTime: Date
    }
    
    // Static data
    var ecoTimerName: String
}

If like me you care about organization, create a new file for the ActivityAttributes and a new folder to group all Attributes together. 😊

4 - Add Live Activities to the widget extension

Live Activities leverage WidgetKit, so add the necessary code to return an ActivityConfiguration in your widget implementation.

struct WidgetExample: Widget {
    let kind: String = "WidgetExample"

    var body: some WidgetConfiguration {
        ActivityConfiguration(for: EcoTimerAttributes.self) { context in
        // TODO: Create the presentation that appears on the Lock Screen and as a
        //banner on the Home Screen of devices that don't support the
        // Dynamic Island.
        // ...
        } dynamicIsland: { context in
            // TODO: Create the presentations that appear in the Dynamic Island.
        // ...
        }
    }
}

Note: If your app already offers widgets, add the Live Activity to your WidgetBundle. If you don’t have a WidgetBundle check this page and create one.

If you selected “Include Live Activities” when you added new widget extension target to your project, Xcode automatically creates a widget bundle for you that includes a widget and a Live Activity.

5 - Create the Lock Screen presentation

Create a new Swift file for the LockScreenView. I recommend you to create a specific file for your view to keep a good project structure.

Here is the code I used to illustrate this example, a very simple view that will show the name and temperature inlined:

//
//  EcoTimerLiveActivityView.swift
//  LiveActivityExample
//
//  Created by Arnaud Derosin on 2023/07/22.
//

import SwiftUI
import ActivityKit
import WidgetKit

struct EcoTimerLockScreenLiveActivityView : View {
    let context: ActivityViewContext<EcoTimerAttributes>

    var body: some View {
        VStack {
            HStack {
                Text(context.attributes.ecoTimerName).font(.headline)
                Spacer()
                Text(context.state.ecoTimerTemperatureAdjust).font(.headline)
            }
            .foregroundColor(Color.init(uiColor: UIColor(red: 0.90, green: 0.84, blue: 0.76, alpha: 1.00)))
        }
        .activitySystemActionForegroundColor(Color.init(uiColor: UIColor(red: 0.90, green: 0.84, blue: 0.76, alpha: 1.00))) // The text color for the auxiliary action button that the system shows next to a Live Activity on the Lock Screen.
        .activityBackgroundTint(Color.init(uiColor: UIColor(red: 0.08, green: 0.14, blue: 0.20, alpha: 1.00))) // Sets the tint color for the background of a Live Activity that appears on the Lock Screen.
        .padding(.horizontal)
    }
}

Note: The system may truncate a Live Activity on the Lock Screen if its height exceeds 160 points.

6 - Create the Dynamic Island presentations

Since Live Activities appear in the Dynamic Island of devices that support it you will have add support and create a view for the:

  • Minimal presentation
  • Compact presentation
  • Expanded presentation

That part is related to your needs and will make this example too long, so I'll skip it but before designing your views I recommend you to have a look to the documentation and follow the design guidelines to propose the best experience for your users.

Bellow an example the structure you have to conform with:

ActivityConfiguration(for: WidgetExampleAttributes.self) { context in
            // Lock screen/banner UI goes here
            EcoTimerLockScreenLiveActivityView(context: context)
        } dynamicIsland: { context in
            DynamicIsland {
                // Expanded UI goes here.  Compose the expanded UI through
                // various regions, like leading/trailing/center/bottom
                DynamicIslandExpandedRegion(.leading) {
                    // Add your view content instead of text
                    Text("DynamicIslandExpandedRegionLeading")
                    .accessibilityLabel("Add an accessibility label here.")
                }
                DynamicIslandExpandedRegion(.trailing) {
                    // Add your view content instead of text
                    Text("DynamicIslandExpandedRegionTrailing")
                    .accessibilityLabel("Add an accessibility label here.")
                }
                DynamicIslandExpandedRegion(.bottom) {
                    // Add your view content instead of text
                    Text("DynamicIslandExpandedRegionBottom")
                    .accessibilityLabel("Add an accessibility label here.")
                }
            } compactLeading: {
                // Add your view content instead of text
                Text("compactLeading")
                .accessibilityLabel("Add an accessibility label here.")
            } compactTrailing: {
                // Add your view content instead of text
                Text("compactTrailing")
                .accessibilityLabel("Add an accessibility label here.")
            } minimal: {
                // Add your view content instead of text
                Text("Minimal")
                .accessibilityLabel("Add an accessibility label here.")
            }
            .widgetURL(URL(string: "")) // Add deeplink to your app
            .keylineTint(Color.red) // Applies a subtle tint color to the surrounding border of a Live Activity that appears in the Dynamic Island.

Note: It's recommended to allow people to customize how they interact with your Live Activity, make sure VoiceOver works correctly for your Live Activity. Consider adding accessibility labels for the SwiftUI view you create for each presentation: Provide accessibility labels

7 - Start / Stop the Live Activity

Before you can start a Live Activity in your app, you need to configure it with an ActivityContent object. It encapsulates the ActivityAttributes structure and additional configuration information:

  • The staleDate tells the system when the Live Activity content becomes outdated.
  • The relevanceScore determines which of your Live Activities appears in the Dynamic Island and the order of your Live Activities on the Lock Screen.

For the purpose of simplifying the example let’s use only 2 buttons. One to start and one to end the Live activity from a view of your app, the update mainly depends on your app logic.

I created a simple View in my App that allow me to start and end the Live Activity from pressing button.

//
//  ContentView.swift
//  LiveActivityExample
//
//  Created by Arnaud Derosin on 2023/07/22.
//

import SwiftUI
import ActivityKit

struct ContentView: View {
    @State private var activity: Activity<EcoTimerAttributes>? = nil
    
    var body: some View {
        // Two simple buttons that will allow us to start / end the LiveActivity
        VStack (spacing: 16) {
            Button("Start Eco mode") {
                // We start the activity pressing the button but you can easily integrate it to your logic.
                startEcoModeActivity()
            }
            .bold()
            .foregroundColor(Color.init(uiColor: UIColor(red: 0.90, green: 0.84, blue: 0.76, alpha: 1.00)))
            .tint(Color.init(uiColor: UIColor(red: 0.08, green: 0.14, blue: 0.20, alpha: 1.00)))
            .cornerRadius(12)

            Button("Stop Eco mode") {
                // We request to stop the activity pressing the button, you can easily integrate it to your logic.
                stopEcoModeActivity()
            }
            .bold()
            .foregroundColor(Color.init(uiColor: UIColor(red: 0.90, green: 0.84, blue: 0.76, alpha: 1.00)))
            .tint(Color.init(uiColor: UIColor(red: 0.08, green: 0.14, blue: 0.20, alpha: 1.00)))
            .cornerRadius(12)

        }
        .buttonStyle(.borderedProminent)
        .controlSize(.large)
    }
    
    // Methods to start and end the activity
    func startEcoModeActivity() {
        let initialContentState = EcoTimerAttributes.ContentState(ecoTimerTemperatureAdjust: "-2°", ecoTimerEndTime: Date().addingTimeInterval(60))
        let attributes = EcoTimerAttributes(ecoTimerName: "Eco mode is running")
    
        let activityContent = ActivityContent(state: initialContentState, staleDate: Date().addingTimeInterval(60))
    
        activity = try? Activity<EcoTimerAttributes>.request(attributes: attributes, content: activityContent)
    }
    
    func stopEcoModeActivity() {
        let initialContentState = EcoTimerAttributes.ContentState(ecoTimerTemperatureAdjust: "-", ecoTimerEndTime: .now)
        let activityContent = ActivityContent(state: initialContentState, staleDate: .now)
        
        Task {
            // End an active Live Activity if exist
            await activity?.end(activityContent, dismissalPolicy: .immediate)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

How the live activity looks on LockScreen

20230726131041

How the live activity is shown in the Dynamic Island (View design skipped as purpose of keeping example short and concise, please enjoy creating awesome UI)

20230726125455

That's it! Simple right? I hope I was able to show you how it works in this short guide and that it will help you to create your Live Activities! 🙏

Note: If the targeted device doesn't support the Dynamic Island the system displays the Lock Screen presentation as a banner if the device is unlocked.

Important to consider

Constraints

  • Can be active for up to eight hours unless your app or a person ends it before.
  • After the 8-hour limit, the system automatically ends it.
  • The system requires image assets to use a resolution that’s smaller or equal to the size of the presentation otherwise the system might fail to start the Live Activity.
  • Each Live Activity runs in its own sandbox, it can’t access the network or receive location updates.
  • The updated dynamic data for both ActivityKit updates and ActivityKit push notifications can’t exceed 4KB in size.

Check document for more detailed informations: Apple

Colors

To allow the user to associate the live activity to the related application graphical layout the goal is to make the Live activity and app looking like it's share the same visual aesthetic.

Live Activity and App icon color consistency

In this example the App icon background and foreground share the same colors. (it’s recommended to drop the Live Activity light theme support if it break the color association)

Sizes

Usage of right amount of space is important, it’s not always needed to show all informations. It’s recommended to show more informations and update the Live activity style only when it’s relevant.

Live Activity updates

When you want to update the Live Activity you can use alerting that will light up the screen and play the standard notification sound to get their attention but be careful about alerting them too often that will encourage them to stop using your app’s Live Activities.

Update a Live Activity only when new content is available, alerting people only if it’s essential to get their attention. It can be disruptive to alert people to a Live Activity update, and alerting them too often — or alerting them to updates that aren’t crucial — can annoy people and encourage them to stop using your app’s Live Activities.

Once the live activity is ended, remove it from the LockScreen after a short duration of time to improve the user experience.

Consider removing a Live Activity from the Lock Screen 15 minutes after it ends. In the Dynamic Island, the system immediately removes a Live Activity when it ends. By default, the system shows a Live Activity on the Lock Screen for up to four hours after it ends to give people time to view its final content update. In many cases, though, the outcome of a Live Activity is only relevant for a shorter time, and 15 minutes is adequate.

Conclusion

Thank you for reading this article, I hope it will help you to get started and create stunning Live Activities. ✨

Follow me if you got interest into these new ways to extend your app usage outside of our app. In my next article I plan to explain about a new way to add interactivity to your Widgets and Live Activities with examples. (a feature that will be introduce with iOS17).