Starting from iOS 16.1 Apple provides a framework called ActivityKit which allows to create lock screen live widgets. One of the biggest problems with home screen widgets is that they do not show actual real-time updates. However, with Live Activities, you can provide real-time updates on the lock screen. This is helpful for apps that need to show time-critical information, like an ongoing workout activity, progress tracking in a food delivery app, or the score of a live-action game.
ActivityKit and Live Activities are kinda similar to WidgetKit and is used to present dynamic information to the user via widgets on the lock screen and Dynamic Island. ActivityKit is availabel on iOS straring since 16.1 version. Actually, it's better to start from iOS 16.2 because Apple changed some vital methods between 16.1 and 16.2. Some methods introduced in 16.1 is now deprecated in 16.2.
Let's familiarize with some technical restrictions
To add Live Activities to the existed project start with adding the widget extension by selecting the File > New > Target... menu option. Next select the Widget Extension option. On the following screen, enter desired name. Before clicking the Finish button, ensure the Include Live Activity option is selected. When prompted, click the Activate button to ensure the widget extension is included in the project.
Also we must enabled Live Activities support in the Info tab. Select your app's target at the top of the Project Navigator panel. On the Info screen, locate the bottom entry in the list of properties and hover the mouse pointer over the item. When the plus button appears, click it to add a new entry to the list. From within the drop-down list of available keys, locate and select the Supports Live Activities option or paste NSSupportsLiveActivities
value.
Also you can manually add following key to the Info.plist file
<key>NSSupportsLiveActivities</key> <true/>
Let's break totorial in following steps
Data source
The Live Activity attributes declare the data structure to be presented and are created using ActivityKit’s ActivityAttributes
class. The attributes have two properties. One property remains static and does not change throughout one Live Activity. The other property is dynamic and may constantly change throughout the Live Activity.
Within the ActivityAttributes
declaration, the dynamic attributes are embedded in a ContentState
structure using the following syntax.
import ActivityKit import SwiftUI // Data struct Quotation: Codable, Hashable { let quiotation: String let author: String static func preview() -> [Quotation] { return [ Quotation(quiotation: "Do one thing every day that scares you.", author: "Eleanor Roosevelt"), Quotation(quiotation: "If you can dream it, you can do it.", author: "Walt Disney"), Quotation(quiotation: "Success is not final, failure is not fatal: it is the courage to continue that counts.", author: "Winston Churchill"), Quotation(quiotation: "The pessimist complains about the wind. The optimist expects it to change. The leader adjusts the sails.", author: "John Maxwell"), Quotation(quiotation: "It is amazing what you can accomplish if you do not care who gets the credit.", author: " Harry Truman") ] } static func random() -> Quotation { preview().randomElement()! } } // Attributes struct QuotationAttributes: ActivityAttributes { // Dynamic values public struct ContentState: Codable, Hashable { var quotation: Quotation } // Static values var quotationName: String }
View
Live Activities present data on lock screen or Dynamic Island. These views are created using SwiftUI views. The lock screen view consists of a single layout, the Dynamic Island views are separated into areas.
import WidgetKit import SwiftUI import ActivityKit struct QuotationWidgetView : View { let context: ActivityViewContext<QuotationAttributes> var body: some View { VStack(alignment: .leading) { Text(context.state.quotation.quiotation) HStack { Spacer() Text(context.state.quotation.author).font(.caption) } } .padding() .activityBackgroundTint(Color.cyan) .activitySystemActionForegroundColor(Color.black) } } struct QuotationWidget: Widget { let kind: String = "QuotationWidget" var body: some WidgetConfiguration { ActivityConfiguration(for: QuotationAttributes.self) { context in QuotationWidgetView(context: context) } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.leading) { Text("Leading") } DynamicIslandExpandedRegion(.trailing) { Text("Trailing") } DynamicIslandExpandedRegion(.bottom) { Text("Bottom") } } compactLeading: { Text("L") } compactTrailing: { Text("T") } minimal: { Text("M") } } } }
Each element has a context object where static and dynamic data values can be accessed for inclusion in the views.
The Live Activity will display data using compact layouts on devices with a Dynamic Island. However, a long press performed on the island will display the expanded widget. Unlike the lock screen widget, the expanded Dynamic Island presentation is divided into four regions.
Start the Live Activity
Once the data model has been defined and the presentations designed, the next step is to request and start the Live Activity. We can do it by calling the Activity.request()
method. We have to pass two params an activity attributes instance and a push type. The push type should be set to token if the data updates will be received via push notifications or nil if updates are coming from the app.
An optional stale date may also be included. It is a date to indicate the iOS when the Live Activity will become outdated. If no staleDate
is passed, after 8 hours, the iOS will end the Live Activity. To check if the Live Activity is out of date, check the isStale
property.
Let's start with checking of Live Activity availability
guard ActivityAuthorizationInfo().areActivitiesEnabled else { print("Activities are not enabled") return }
Next step is to create an activity attributes object and initialize any static properties, for example:
let attributes = QuotationAttributes(quotationName: "Motivation")
The second requirement is a ContentState
instance configured with initial dynamic values
let quotation = Quotation.random() let state = QuotationAttributes.ContentState(quotation: quotation) activity = try? Activity<QuotationAttributes>.request( attributes: attributes, content: .init(state: state, staleDate: nil), pushType: nil )
If the request is successful, the Live Activity will launch and be ready to receive updates.
Update the Live Activity
There three ways to update Live Activity with new data
staleDate
and run AppIntentTo update a Live Activity from the app you need to call the update()
method of the activity instance returned by the Activity.request()
method. The update call must be passed an ActivityContent
instance containing a ContentState
object with new dynamic data values and an optional stale date value.
let quotation = Quotation.random() let state = QuotationAttributes.ContentState(quotation: quotation) Task { await activity?.update(.init(state: state, staleDate: nil, relevanceScore: 0)) }
The iOS will display the Live Activity with the highest relevanceScore
.
Stop the Live Activity
Live Activity is stopped by calling the end()
method of the activity instance. You need to pass a ContentState
object with the final data values and a dismissal policy setting.
let quotation = Quotation.random() let state = QuotationAttributes.ContentState(quotation: quotation) Task { await activity?.end(.init(state: state, staleDate: nil), dismissalPolicy: .immediate) }
When the dismissalPolicy
is set to default
, the Live Activity widget will remain on the lock screen up to 12 hours unless the user removes it. Use immediate
to instantly remove the Live Activity from the lock screen.
You can check code on Github.