Adding a Refresh Button to an iOS 17 Live Activity

The previous chapters introduced Live Activities and created an example project demonstrating Live Activities in action. While testing the example app, it became evident that, regardless of configuration settings, Live Activity updates can be infrequent when the app is in background mode. While we have limited control over when an update will be processed, this chapter will demonstrate how to compensate for this unpredictability by informing the user that information may be outdated and providing a convenient way to refresh the data from within the Live Activity widget.

Adding Interactivity to Live Activities

In addition to displaying information, Live Activity widgets may contain buttons and toggle views using the App Intents framework. This involves adding a class to the Live Activity widget extension that conforms to the LiveActivityIntent protocol and implementing a perform() method to be called when the user interacts with the button or toggle. For example:

struct UpdateScoreIntent: LiveActivityIntent {
    
    static var title: LocalizedStringResource = "Check for Updates"
    static var description = IntentDescription("Update latest match scores.")
    
    public init() { }
    
    func perform() async throws -> some IntentResult {
        // Code to perform the update goes here
        return .result()
    }
}Code language: Swift (swift)

Once the intent has been declared, we can call it from within the Live Activity widget. The following code excerpt, for example, shows a Button view configured to call the UpdateScoreIntent intent:

Button(intent: UpdateScoreIntent(), label: {
    Text("Update")
})Code language: Swift (swift)

In the rest of this chapter, we will modify the LiveActivityDemo project to provide a notice and a refresh button when the widget is displaying outdated information. When the button is tapped, it will launch an intent to update the Live Activity with the latest data.

Adding the App Intent

Load the LiveActivityDemo project into Xcode, right-click on the DemoWidget folder in the Project navigator panel, and select the New File… option. Create a new Swift file named RefreshIntent.swift, making sure to enable the LiveActivityDemo and DemoWidgetExtension targets, as shown in Figure 1-1:

Figure 1-1

After creating the file, modify it so it reads as follows:

import Intents
import AppIntents

struct RefreshIntent: LiveActivityIntent {
    
    static var title: LocalizedStringResource = "Refresh Pricing"
    static var description = IntentDescription("Present the latest pricing.")
    
    public init() { }
    
    func perform() async throws -> some IntentResult {
        return .result()
    }
}Code language: Swift (swift)

For this example, we only need to bring the app out of background mode so that the view model timer will update the stock price data. Though more complex apps will need the method to perform some tasks, calling the perform() method, even if all it does is immediately return, is enough to wake up the app and update the data.

Setting a Stale Date

We only need the refresh button to appear when the displayed data is outdated. We can do this by specifying a stale date when the Live Activity starts and each time it is updated. Edit the PricingViewModel.swift file and modify the activity request() and update() code to set a stale date:

func startTracking(ticker: String) {
.
.
    do {
        activity = try Activity.request(
            attributes: attributes,
            content: .init(state: contentState, staleDate: Date.now + 15),
            pushType: nil
        )
        startUpdates()
    } catch (let error) {
.
.
private func update() {
.
.
        await activity?.update(
            ActivityContent<DemoWidgetAttributes.ContentState>(
                state: priceStatus,
                staleDate: Date.now + 15,
                relevanceScore: 0
            ),
            alertConfiguration: alertConfig
        )
    } 
.
.Code language: Swift (swift)

Detecting Stale Data

The next step is to display a notice and a refresh button in the lock screen widget presentation layouts (the same steps may be followed for the Dynamic Island expanded presentation) when the Live Activity context’s isStale property is set to true. Modify the LockScreenView.swift file as follows to add this behavior:

.
.
    var body: some View {
        let color = context.state.changePercent < 0 ? Color.red : Color.green
        
        VStack(alignment: .leading) {
            HStack {
                
                VStack(alignment: .leading, spacing: 8) {
                    Text(context.attributes.ticker).bold()
                        .font(.title2)
                    
                    Text("\(context.state.currentPrice, specifier: "%.2f")")
                        .font(.title3)
                        .foregroundColor(color)
                }
                
                Spacer()
                
                Text("\(context.state.changePercent, specifier: "%.0f")%")
                    .font(.largeTitle)
                    .foregroundColor(color)
                
                Spacer()
                
                PercentView(context: context)
                    .padding()
                
            }
            
            if (context.isStale) {
                HStack { 
                    Text("Update required ")
                    Button(intent: RefreshIntent(), label: {
                        Image(systemName: "arrow.clockwise.circle.fill")
                    })
                    Spacer()
                }
            }
        }
        .padding()
        .activityBackgroundTint(Color.black)
        .activitySystemActionForegroundColor(Color.white)
    }
}Code language: Swift (swift)

Testing the Live Activity Intent

Run the app on a device or emulator, then stop the app from within Xcode to detach from the debugger. Relaunch the app from the device or emulator screen and start tracking. Place the app in the background and lock the device. Wait until the lock screen widget displays the update required message and the refresh button, as shown in Figure 1-2:

Figure 1-2

Tap the refresh button to update the pricing data. If you are testing on a physical device, iOS must verify your identity before the refresh can be performed. The camera will need to see your face on devices with Face ID before the widget updates.

Summary

In addition to displaying information, Live Activity widgets may contain interactive buttons and toggle views. These views can be configured to trigger actions within the Live Activity extension via an App Intent. When combined with the Live Activity stale date property, this is a helpful way to notify the user of outdated data and provide a button to perform a refresh from within the widget.


Categories