Apple recently announced the Apple Watch Ultra, which adds an additional button to the Apple Watch.
One of the primary uses for this button is to control (start / pause / resume) workouts.
We've recently added this functionality into Streaks Workout, so this article shares what we've learned in doing so.
Firstly, your app must have the Workout Processing Background Mode (WKBackgroundModes
must contain workout-processing
).
Secondly, your app must provide an implementation of StartWorkoutIntent
.
Define the Workout Style Value
The first thing you need to do is create a workout style, which must either be an AppEntity
or AppEnum
. In Streaks Workout, we are using the user-defined custom workouts, so we use AppEntity
. If you have a fixed list of workout types then using AppEnum
would be simpler.
import AppIntents
struct MyWorkoutAppEntity: AppEntity {
var id: UUID
var title: String
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Workout Type"
var displayRepresentation: DisplayRepresentation {
.init(stringLiteral: title)
}
static var defaultQuery = MyWorkoutAppEntityQuery()
}
Since AppEntity
requires a query type, you must also implement this.
struct MyWorkoutAppEntityQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [MyWorkoutAppEntity] {
let workouts = try await suggestedEntities()
return workouts.filter { identifiers.contains($0.id) }
}
func suggestedEntities() async throws -> [MyWorkoutAppEntity] {
// Return your list of workout types here
[
WorkoutStyle(id: .runningIdentifier, title: "Running")
]
}
}
Create the Start Workout Intent
This works just like other iOS 16 App Intents, in that you do the hard lifting inside the perform()
method.
In Streaks Workout, we also want the app to run in the foreground, so we set openAppWhenRun
to true
.
We're not using an App Intents extension in Streaks Workout so this is code is all added to the main Apple Watch extension.
import AppIntents
struct MyStartWorkoutIntent: StartWorkoutIntent {
typealias WorkoutStyle = MyWorkoutAppEntity
@Parameter(title: "Workout")
var workoutStyle: WorkoutStyle
init() {
workoutStyle = .init(id: .defaultIdentifier, title: "Defaut Workout")
}
static var openAppWhenRun: Bool { true }
static var suggestedWorkouts: [MyStartWorkoutIntent] {
// Return configured workout intents here
[
MyStartWorkoutIntent(
style: WorkoutStyle(id: .runningIdentifier, title: "Running")
)
]
}
var displayRepresentation: DisplayRepresentation {
workoutStyle.displayRepresentation
}
static var title: LocalizedStringResource = "Start a Workout"
static var parameterSummary: some ParameterSummary {
Summary("Start a \($\.workout) workout")
}
func perform() async throws -> some IntentResult {
// Start your workout here
return .result()
}
}
A few things to note in the above code:
- For the code to compile, you must define a default workout style in the
init()
method. - For the Action Button configuration screen to display your workouts, you must define the
var workoutStyle
as a@Parameter
. This is good practice anyway, since it allows you to then use this App Intent in the Shortcuts app. - The
suggestedWorkouts
variable is not asynchronous, so you can't simply just callMyWorkoutAppIntentQuery.suggestedEntities()
here. You need to maintain a non-asynchronous list somewhere.
Implementing Pause and Resume
These are a bit simpler to implement, since no parameters or custom entities are required:
struct MyPauseWorkoutIntent: PauseWorkoutIntent {
static var title: LocalizedStringResource = "Pause Workout"
func perform() async throws -> some IntentResult {
// Pause the workout
return .result()
}
}
struct MyResumeWorkoutIntent: ResumeWorkoutIntent {
static var title: LocalizedStringResource = "Resume Workout"
func perform() async throws -> some IntentResult {
// Resume the workout
return .result()
}
}
Migrating from Previous INIntent
Before Apple Watch Ultra and StartWorkoutIntent
was announced, we already had a Shortcut for starting a workout in Streaks Workout, called WorkoutStartIntent
.
We want to keep this for users running older version of iOS and Apple Watch, but we don't want both the old and this new one to appear in the Shortcuts app.
To solve this, we changed our definition to implement CustomIntentMigratedAppIntent
:
struct MyStartWorkoutIntent: StartWorkoutIntent, CustomIntentMigratedAppIntent {
// This is the name in the .intentsdefinition file
static var intentClassName: String = "WorkoutStartIntent"
// ...
}
Configuring as a Shortcut
Part of the new App Intents system is to create an AppShortcutsProvider
. There is plenty of other detailed material about doing this, but to round out our implementation we also included our intents here.
struct MyAppShortcuts: AppShortcutsProvider {
static var shortcutTileColor: ShortcutTileColor {
.orange
}
@AppShortcutsBuilder
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: MyStartWorkoutIntent(),
phrases: [
"Start \(\.$workoutStyle) in \(.applicationName)"
]
)
AppShortcut(
intent: MyPauseWorkoutIntent(),
phrases: [
"Pause \(.applicationName)"
]
)
AppShortcut(
intent: MyResumeWorkoutIntent(),
phrases: [
"Resume \(.applicationName)"
]
)
}
}
That's all there is to it! Now you can go to the Apple Watch Settings app, then go to Action Button to configure the button for your app.