SDKs
- iOS 11.0+
- Xcode 10.2+
Frameworks
- Intents
- Intents
UI
Overview
The app in this sample project allows users to start, pause, resume, cancel, and end custom wall-climbing or bouldering workouts with Siri, as well as view historical workout data inside the main app.
The project consists of three targets:
Ascent
, an iOS app that displays a history of climbing and bouldering workouts.Ascent
, an Intent Extension that integrates with SiriKit to control workouts.Intents Extension Ascent
, an embedded framework containing shared code needed by both the Intent Extension and the main app.Framework
See Creating an Intents App Extension for more information on the general process of adding an Intent Extension to your app, including how to enable the Siri capability and configure the NSExtension
keys in Info
.
Configure the Sample Code Project
This sample app can be run in the iOS Simulator without any special setup, but in order to run it on a device you will need to update the build settings and enable an App Group for the project:
Open
Ascent
with the latest version of Xcode..xcodeproj In the project editor, set a new bundle identifier under Identity on the General pane for each of the three targets in the project.
In the Capabilities pane, make sure that App Groups is switched on for the
Ascent
andAscent
targets.Intents Extension Add an App Group identifier with the format
group
..com .example Reference the new App Group identifier in the source code. Open
Workout
and modify the implementation of theHistory .swift shared
property to reference the new identifier.User Defaults
Note
Each target in the project must preface its bundle identifier with the bundle identifier of the app’s main target. For example, if you set the bundle identifier for the Ascent
target to com
, you should set the bundle identifier for the Ascent
target to com
.
Recognize and Respond to Workout Requests
In order for the Intent Extension to handle workout requests, the principal class of the extension must implement the handler(for:)
method of INIntent
. Use this method to dispatch the request to a class configured to handle it.
override func handler(for intent: INIntent) -> Any {
for handler in intentHandlers where handler.canHandle(intent) {
return handler
}
preconditionFailure("Unexpected intent type")
}
In order for a class to handle a request from SiriKit to use the workout controls, the class must implement one of the workout intent handler protocols. In this sample project:
Start
implementsWorkout Intent Handler INStart
.Workout Intent Handling Pause
implementsWorkout Intent Handler INPause
.Workout Intent Handling Resume
implementsWorkout Intent Handler INResume
.Workout Intent Handling Cancel
implementsWorkout Intent Handler INCancel
.Workout Intent Handling End
implementsWorkout Intent Handler INEnd
.Workout Intent Handling
Resolve Parameters
The first step of handling a request from Siri with one of these classes is to resolve any parameters from the voice command. For example, when starting a workout the user may specify parameters such as the type of workout, a particular goal, and the location of the workout:
“Start a 30 minute outdoor wall climb with Ascent” specifies the workout goal, location, and name.
“Start a wall climb workout with Ascent” specifies only the name of the workout.
“Start a workout with Ascent” specifies no parameters, only that a workout should be started.
Your app must decide how to handle these situations using the available resolution methods for the intent type. To handle parameter resolution for INStart
, implement one or more of the resolve
, resolve
, resolve
, resolve
, or resolve
methods.
In each of these calls, return an appropriate resolution result value by calling the completion
block. For example, when handling the resolution of the workout name, return an INSpeakable
such as success(with:)
, confirmation
, disambiguation(with:)
, or needs
.
func resolveWorkoutName(for intent: INStartWorkoutIntent, with completion: @escaping (INSpeakableStringResolutionResult) -> Void) {
let result: INSpeakableStringResolutionResult
let workoutHistory = WorkoutHistory.load()
if let name = intent.workoutName {
// Try to determine the obstacle (wall or boulder) from the supplied workout name.
if Workout.Obstacle(intentWorkoutName: name) != nil {
result = INSpeakableStringResolutionResult.success(with: name)
} else {
result = INSpeakableStringResolutionResult.needsValue()
}
} else if let lastWorkout = workoutHistory.last {
// A name hasn't been supplied so suggest the last obstacle.
result = INSpeakableStringResolutionResult.confirmationRequired(with: lastWorkout.obstacle.intentWorkoutName)
} else {
result = INSpeakableStringResolutionResult.needsValue()
}
completion(result)
}
Note
It is not necessary to implement all of the available parameter resolution methods, only those that need additional logic. For example, the extension in this sample does not need to do any special processing of the goal value when starting a workout, so there is no implementation of resolve
, although the app still recognizes and processes the goal value.
To aid Siri with recognition of parameter names like “wall climb” and “boulder climb,” add an App
to your project. More information on this file and how it can add vocabulary to Siri can be found in Registering Custom Vocabulary with SiriKit.
Once all parameters have been resolved, the system calls confirm(intent:
. Validate the resolved parameters and pass back an INStart
with an INStart
.
Complete the Request
Once the request to start the workout has been confirmed, the system calls handle(intent:
. Some requests from Siri will be better handled by the main app rather than the Intent Extension; for example, the main app can better respond to requests in which long-lived system services are needed to support the workout.
To transfer control to the main app, create a INStart
with the INStart
code and pass it to the completion block.
func handle(intent: INStartWorkoutIntent, completion: @escaping (INStartWorkoutIntentResponse) -> Void) {
// `handleInApp` will transfer handling this activity to the main app and deliver the intent to the app delegate in the background.
let response = INStartWorkoutIntentResponse(code: .handleInApp, userActivity: nil)
completion(response)
}
When an Intent Extension handles an intent with the handle
response code, it delivers the to the intent to the application(_:
method in the main app’s process in the background.
func application(_ application: UIApplication, handle intent: INIntent, completionHandler: @escaping (INIntentResponse) -> Void) {
if let intent = intent as? INStartWorkoutIntent {
completionHandler(handle(intent))
} else if let intent = intent as? INCancelWorkoutIntent {
completionHandler(handle(intent))
} else if let intent = intent as? INPauseWorkoutIntent {
completionHandler(handle(intent))
} else if let intent = intent as? INEndWorkoutIntent {
completionHandler(handle(intent))
} else if let intent = intent as? INResumeWorkoutIntent {
completionHandler(handle(intent))
} else {
preconditionFailure("Trying to handle unknown intent type")
}
}