Responding to the Action button on Apple Watch Ultra
Use App Intents to register actions for your app.
Overview
On Apple Watch Ultra, people can specify the action that the system performs when they press the Action button. By default, the watch provides actions for starting workouts, starting dives, starting a backtrack navigation session, setting a waypoint, turning on the flashlight, and running a shortcut from the Shortcuts app. To add your dive or workout app to the list of apps that appear when someone chooses Settings > Action Button, use App Intents to register actions for your app.
For workout apps, you can implement the following protocols:
- StartWorkoutIntent
Starts a workout session when someone first presses the Action button.
- AppIntent
Runs a custom action when someone presses the Action button during your app’s active workout session. Donating an intent sets the Action button’s next action. Your app can have only one next action at a time; however, donating a new intent changes the next action.
- PauseWorkoutIntent
Pauses the current workout session when someone simultaneously presses the Action button and the side button during a workout session.
- ResumeWorkoutIntent
Resumes the current workout session when someone simultaneously presses the Action button and the side button while a workout session is in a paused state.
For dive apps, you can implement the following protocols:
- StartDiveIntent
Starts a dive session when someone first presses the Action button.
- AppIntent
Runs a custom action when someone presses the Action button during your app’s active dive session. The system also performs the next action if someone simultaneously presses the Action button and the side button. Donating an intent sets the Action button’s next action. Your app can have only one next action at a time; however, donating a new intent changes the next action.
Start a new workout session
Start by creating either an AppEnum or an AppEntity that defines the types of workouts that your app supports. If your app supports only a single workout, you can create an enumeration with a single case. Also define the display representation for each type of workout that your app supports. Apple Watch Ultra shows the case description’s title and subtitle below the First Press settings when someone sets your app as the workout app in Settings > Action Button.
enum WorkoutEnum: String, AppEnum {
// List the types of workout your app supports.
case running
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Workout"
// Define the display representation for each of the workouts your app supports.
static var caseDisplayRepresentations: [WorkoutEnum: DisplayRepresentation] =
[.running: DisplayRepresentation(title: "Running", subtitle: "outside run")]
}Next, create a structure that adopts the StartWorkoutIntent protocol. Your implementation needs to define the intent’s title, a list of suggestedWorkouts, and a parameter that contains the workoutStyle.
struct MyStartWorkoutIntent: StartWorkoutIntent {
// Define the intent's title.
static var title: LocalizedStringResource = "Start Workout"
// Define a list of start workout intents that appear below the First Press settings when someone sets your app as the workout app in Settings > Action Button.
static var suggestedWorkouts: [MyStartWorkoutIntent] = [MyStartWorkoutIntent()]
// Define a parameter that specifies the type of workout that this intent starts.
@Parameter(title: "Type of Workout")
var workoutStyle: WorkoutEnum
// Define an init method that sets the default workout type.
init() {
workoutStyle = .running
}
// Add the display representation, and the perform method here.
}You can dynamically change the list of suggested workouts by changing the value of the suggestedWorkouts property and then calling invalidateSuggestedWorkouts(), which tells the system to reread the suggested workouts.
Next, set the display strings for the intent by defining the displayRepresentation just after the intent’s initializer.
var displayRepresentation: DisplayRepresentation {
WorkoutEnum.caseDisplayRepresentations[workoutStyle] ??
DisplayRepresentation(title: "Unknown")
}Then, implement your intent’s perform() method. The system calls this method when anything starts the intent. In your implementation, you have 30 seconds to start a workout session and return a successful value. If you don’t start a workout session in that time, the system displays an error message, but the app remains in the foreground. People can start a workout session directly from the app, but without a session, the app goes to the background the next time they drop their wrist.
// Define the method that the system calls when it performs the intent.
func perform() async throws -> some IntentResult {
logger.debug("*** Performing a Start Intent. ***")
// Start a workout session inside the perform method.
let workoutManager = MyWorkoutManager.shared
try await workoutManager.startWorkout(type: workoutStyle)
// Schedule a task to request authorization and then set up the data source and start collecting data from the workout.
Task {
await workoutManager.requestAuthorization()
do {
try await workoutManager.startCollectingData()
} catch {
fatalError("*** An error occurred while setting up the data source: \(error.localizedDescription) ***")
}
}
// Return a successful result.
return .result()
}The start workout intent becomes available as soon as someone downloads your app. This means they can set up the Action button and run your start workout intent without ever launching your app.
Similarly, if you update your app and change the data types that the workout session uses, someone can launch the updated workout from the Action button without launching your app.
To ensure that your app requests authorization for the current set of HealthKit data types it intends to use during its workout sessions, you need to schedule an authorization request from within your intent’s perform() method. Because someone using your app doesn’t have access to its user interface until after the perform() method returns, you need to schedule the request authorization using a Task.
Before authorizing the HealthKit data, create and start your workout session.
func startWorkout(type: WorkoutEnum) throws {
logger.debug("*** Start a workout of type \(type.rawValue) ***")
logger.debug("==> Creating the workout configuration.")
let configuration = HKWorkoutConfiguration()
configuration.activityType = .running
configuration.locationType = .outdoor
self.configuration = configuration
logger.debug("==> Creating the workout session.")
let session = try HKWorkoutSession(healthStore: store, configuration: configuration)
session.delegate = self
self.session = session
workoutType = type
logger.debug("==> Starting the session.")
session.startActivity(with: Date())
}The code example above creates a workout configuration for an outdoor run. It then uses the configuration to create the workout session, assigns a workout session delegate to receive state change and errors from the workout, and starts the session.
Because the app hasn’t created a data source for the workout session, the session doesn’t generate any data.
Next, request authorization for all the HealthKit data types that your workout sessions use.
func requestAuthorization() async {
logger.debug("*** Requesting Authorization ***")
// The quantity type to write to the health store.
let typesToShare: Set = [
HKQuantityType.workoutType()
]
// The quantity types to read from the health store.
let typesToRead: Set = [
HKQuantityType(.heartRate),
HKQuantityType(.activeEnergyBurned),
HKQuantityType(.distanceWalkingRunning)
]
guard HKHealthStore.isHealthDataAvailable() else {
logger.debug("*** HealthKit is not supported on this device. ***")
return
}
do {
try await store.requestAuthorization(toShare: typesToShare, read: typesToRead)
} catch {
fatalError("*** An error occurred while requesting authorization to read and save data: \(error.localizedDescription) ***")
}
enabled = true
}This authorization request can take an arbitrarily long amount of time. Any time you request authorization for new data, the system displays an authorization sheet, and waits until someone either authorizes the data or dismisses the sheet. However, if someone has already authorized the requested data types, the system returns immediately.
After the authorization request finishes, set up the data source, assign a delegate to receive data from the workout builder, and begin collecting data from the workout.
func startCollectingData() async throws {
precondition(enabled == true)
guard let configuration else { fatalError("*** You need to create a workout configuration before calling this method. ***") }
guard let session else { fatalError("*** You need to create a session before calling this method. ***") }
logger.debug("==> Setting the session's data source.")
let builder = session.associatedWorkoutBuilder()
builder.dataSource = HKLiveWorkoutDataSource(healthStore: store,
workoutConfiguration: configuration)
builder.delegate = self
logger.debug("==> Begin collecting data.")
try await builder.beginCollection(at: Date())
self.builder = builder
logger.debug("==> Donate the mark lap intent as the Action button's next action.")
try await MyStartWorkoutIntent().donate(result: .result(actionButtonIntent: MyMarkLapIntent()))
}In the code example above, the last line donates the MyMarkLapIntent() as the next action for the Action button. Donating the next action is described in more detail below.
After implementing your StartWorkoutIntent, build and run your app to load it onto the test device or Simulator. Then, on the test device, choose Settings > Action Button. Tap Action and choose Workout, then tap App and choose your app.
[Image]
The device starts a new workout session when you press the Action button.
Support multiple workout types
Your app can provide a list of suggested workout types, letting people associate a particular workout type with the Action button.
To suggest multiple workout types, start by defining the different types of workouts that your app supports in your AppEnum implementation.
enum WorkoutEnum: String, AppEnum {
// List the types of workout your app supports.
case walking
case running
case swimming
case cycling
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Workout"
// Set the display representation here.
}Then set a display representation for each case.
static var caseDisplayRepresentations: [WorkoutEnum: DisplayRepresentation] =
[.walking: DisplayRepresentation(title: "Walking", subtitle: "outside walk"),
.running: DisplayRepresentation(title: "Running", subtitle: "outside run"),
.swimming: DisplayRepresentation(title: "Swimming", subtitle: "lap swim"),
.cycling: DisplayRepresentation(title: "Cycling", subtitle: "outside cycling")
]Next, in your StartWorkoutIntent implementation, define the set of suggested workouts.
static var suggestedWorkouts: [MyStartWorkoutIntent] =
[MyStartWorkoutIntent(style: .walking),
MyStartWorkoutIntent(style: .running),
MyStartWorkoutIntent(style: .swimming),
MyStartWorkoutIntent(style: .cycling)]Then, in the perform() method, check the intent’s workoutStyle and create the corresponding workout session.
func perform() async throws -> some IntentResult {
let workoutManager = MyWorkoutManager.shared
await workoutManager.requestAuthorization()
await workoutManager.startWorkout(type: workoutStyle)
return .result()
}Create a configuration for the specified type of workout.
func startWorkout(type: WorkoutEnum) throws {
logger.debug("*** Should be starting a workout of type \(type.rawValue) ***")
logger.debug("==> Creating the workout configuration.")
let configuration = HKWorkoutConfiguration()
switch type {
case .walking:
configuration.activityType = .walking
case .running:
configuration.activityType = .running
case .swimming:
configuration.activityType = .swimming
case .cycling:
configuration.activityType = .cycling
}
configuration.activityType = type.activityType()
if type == .swimming {
configuration.locationType = .indoor
configuration.swimmingLocationType = .pool
configuration.lapLength = HKQuantity(unit: HKUnit.yard(), doubleValue: 25.0)
} else {
configuration.locationType = .outdoor
}
self.configuration = configuration
logger.debug("==> Creating the workout session.")
let session = try HKWorkoutSession(healthStore: store, configuration: configuration)
session.delegate = self
self.session = session
workoutType = type
logger.debug("==> Starting the session.")
session.startActivity(with: Date())
}And, finally, request authorization for all the data types that your workout sessions use.
// The quantity types to read from the health store.
let typesToRead: Set = [
HKQuantityType(.heartRate),
HKQuantityType(.activeEnergyBurned),
HKQuantityType(.distanceCycling),
HKQuantityType(.distanceSwimming),
HKQuantityType(.distanceWalkingRunning)
]Build and run your app again. When you open the Action button settings, you can specify a particular workout for the First Press > Workout setting.
[Image]
And pressing the Action button launches the selected type of workout.
Donate the next action
Apple Watch Ultra runs the next action when someone presses the Action button while a workout or dive session is already running. This means the first time someone presses the Action button, the system starts your session. If they press the Action button any other time during the session, it performs the next action.
To set the next action, implement a structure that adopts the AppIntent protocol.
struct MarkLapIntent: AppIntent {
static var title: LocalizedStringResource = "Mark Lap"
func perform() async throws -> some IntentResult {
logger.debug("*** Perform a mark lap intent. ***")
await MyWorkoutManager.shared.markLap(at: Date())
return .result()
}
}This intent needs a title property that provides a localized description of the action, and a perform() method, which the system calls when it performs the intent.
Next, donate the app intent as the current session’s next action. For example, in your start workout intent’s perform() method, you can donate the next action for the new session by returning result(actionButtonIntent:).
However, in most cases you want to donate a next action regardless of whether the user presses the Action button or launches the session from within your app. To ensure that your app donates the correct intent, simply return .result() from your start intent, and then donate the next intent as soon as the session starts.
Previous code examples show how to donate the MyMarkLapIntent in its startCollectingData() method. The app then calls this method when starting a workout from the Action button or from the app’s user interface.
logger.debug("==> Donate the mark lap intent as the Action button's next action.")
try await MyStartWorkoutIntent().donate(result: .result(actionButtonIntent: MyMarkLapIntent()))You can donate as many action intents as you need; however, your app can use only the most recently donated intent as the next action. Donating a new intent changes the next action.
Pause and resume a workout
Apple Watch Ultra supports pausing and resuming a current workout session by simultaneously pressing both the Action button and the side button.
To implement the pause action, create a structure that adopts the PauseWorkoutIntent protocol.
struct MyPauseWorkoutIntent: PauseWorkoutIntent {
static var title: LocalizedStringResource = "Pause Workout"
func perform() async throws -> some IntentResult {
logger.debug("*** Performing a pause intent. ***")
await MyWorkoutManager.shared.pauseWorkout()
return .result()
}
}This intent needs a title property that provides a localized description of the action, and a perform() method, which the system calls when it performs the intent.
Similarly, to implement the resume action, create a structure that adopts the ResumeWorkoutIntent protocol.
struct MyResumeWorkoutIntent: ResumeWorkoutIntent {
static var title: LocalizedStringResource = "Resume Workout"
func perform() async throws -> some IntentResult {
logger.debug("*** Performing a resume intent. ***")
await MyWorkoutManager.shared.resumeWorkout()
return .result()
}
}If your app doesn’t implement structures that adopt these protocols, the system ignores simultaneous presses.
Start a new dive session
Dive sessions work similarly to workout sessions. To start a dive session, implement a structure that adopts the StartDiveIntent protocol. Typically, people start the dive session just before entering the water. Your app can then donate App Intents that help them use your app while in the water. For example, while in the water they can’t use the touch screen, but the Action button and Digital Crown function normally.
struct MyStartDiveSessionIntent: StartDiveIntent {
static var title: LocalizedStringResource = "Starting a dive session."
func perform() async throws -> some IntentResult {
logger.debug("*** Starting a dive session. ***")
await DiveManager.shared.start()
return .result(actionButtonIntent: MyCollectSubmergedDataIntent())
}
}This intent needs a title property that provides a localized description of the action, and a perform() method, which the system calls when it performs the intent.
To read live depth, water pressure, and water temperature data, see Accessing submersion data.
Unlike workout sessions, the start dive intent supports only a single type of session. You can donate next actions for your dive session; however, you can’t create intents to pause or resume the session.
Passing data to the intents
The previous examples use singleton objects to share data between the different parts of your app; however, App Intents support dependency injection, letting you define the data in your main app, and access it in your intents.
In your intents, create a property that uses the AppDependency property wrapper.
struct MyStartWorkoutIntent: StartWorkoutIntent {
@Dependency var workoutManager: MyWorkoutManager
// Add remaining code here.
}Then, as early as possible when your app launches, use the AppDependencyManager to define the dependency.
AppDependencyManager.shared.add { MyWorkoutManager() }You can set the dependency in your app’s applicationDidFinishLaunching() method. Or, if your app uses SwiftUI life cycles, add an init() method to your App structure, and set it there.
One of the main advantages of dependency injection is that you can easily replace your main data object with a mock object during testing.
Debug intents
To see console output and respond to breakpoints in Xcode, use the following steps to debug your intents:
Build and run your app in Xcode.
On the test device or Simulator, send your app to the background. For example, press Shift-Command-H in Simulator to send your app to the background.
Make sure you set the actions you want to test by choosing Settings > Action Button.
Press the Action button to test the primary and next actions. On a test device, you can also test the pause and resume actions for workout sessions by pressing the Action button and the side button simultaneously.
Any logging from your intents appears in Xcode’s console. The system also pauses execution for any breakpoints you set in Xcode.