Demystifying Siri, Part 5: Intents Extensions
After a somewhat circuitous route we are now finally ready to implement our interactive voice-based interface to NumberRace. At the end of this part, Siri will ask us for a target number and six initial numbers and read out a solution.
Mea culpa
First, though, a confession. Is this really the best use of a voice interface? According to Apple’s Human Interface Guidelines, Siri Shortcuts should be used to accelerate common actions. The guidelines suggest designing intents that require as few follow-up questions as possible. We have seven parameters to complete before Siri is able to provide a solution. Are we really saving a user time by providing a voice-based interface with seven questions to answer? Well, in my defence, we could theoretically run our shortcut on a HomePod or Apple Watch so we’re making our solver available on more devices. We’ll press on and see how cumbersome our shortcut is in action.
Intents Extensions
Our app interacts with Siri through an Intents extension. This extension does the job of retrieving the responses from Siri, identifying missing parameters, and supplying the results of our intent.
We’ll be adding a new Intents extension target to our project shortly, but first there are a few things we need to do before we get coding. Firstly, we’ll create an App Group so that the app and the Intents extension can share resources between them. Use the Apple Developer Portal to create an App Group and assign it to one or more App IDs. Add the App Group to your project target by navigating to the Signing & Capabilities section of your target’s settings in Xcode.
Secondly, you’ll need to ensure that the Siri capability is enabled. While you’re in the Signing & Capabilities section, click on the + Capability button and select Siri from the list.
In my NumberRace app, I have a Solver
class that does all the calculations needed to generate a solution. It’d be great if this code could be shared by both my app and the Intents extension to save space. I’ve therefore moved this class into a framework to save having to include it in both targets.
I won’t trouble you with the steps involved in moving your code into a framework as there are plenty of tutorials online. I used this one from Ray Wenderlich.
A new target
Next, go to File > New > Target… and select Intents Extension. Then click Next.
Let’s call our extension NumberRaceIntents
and we’ll check the Include UI Extension box while we’re at it.
Next, add our App Group to the NumberRaceIntents and NumberRaceIntentsUI targets.
You’ll recall that in Part 3 we spoke about our .intentdefinition
file being translated into custom generated classes. We’ll need to ensure that our new extensions have access to these classes. Click on Intents.intentdefinition
and, in the right hand pane under Target Membership, ensure that our new extensions are checked and that Public Intent Classes is selected for each.
Finally, we need to specify the list of intents that our extension will handle. Go to the Target settings, and in the General section, add SolveGameIntent
to the list of supported intents.
Writing some code
Returning to our Intents extension, a default IntentHandler.swift
has already been created. In this default handler, we’ll check to see if our SolveGame intent has been invoked and use a Solve Game-specific handler instead to perform all the tasks we need.We’ll create that specific handler in a moment, but for now, let’s change our intent handler code to the following:
class IntentHandler: INExtension {
override func handler(for intent: INIntent) -> Any {
// This is the default implementation. If you want different objects to handle different intents,
// you can override this and return the handler you want for that particular intent.
guard intent is SolveGameIntent else {
fatalError("Unhandled intent type: \(intent)")
}
return SolveGameIntentHandler()
}
}
Our handler will react to requests from Siri when it needs assistance in handling our intent. This means:
- Resolving intent data - letting Siri know that data is missing or invalid so that Siri can ask the user for clarification
- Handling the request once all data has been resolved, passing the results back to Siri for reading out
Let’s start with the first of those items - resolving data.
Resolving data
Let’s create a new file in our Intents target, SolveGameIntentHandler.swift
. We’ll start off by resolving our initial numbers. Add the following code to our file:
import Foundation
import Solver
class SolveGameIntentHandler: NSObject, SolveGameIntentHandling {
func resolveInitialNumber(_ initialNumber: InitialNumber) -> InitialNumberResolutionResult {
if initialNumber.rawValue != 0 {
print(initialNumber.rawValue)
return InitialNumberResolutionResult.success(with: InitialNumber(rawValue: initialNumber.rawValue)!)
} else {
return InitialNumberResolutionResult.needsValue()
}
}
func resolveNumber1(for intent: SolveGameIntent, with completion: @escaping (InitialNumberResolutionResult) -> Void) {
completion(resolveInitialNumber(intent.number1))
}
}
Here we’ve written a function to resolve an initial number, resolveInitialNumber()
, and we’re using it to resolve our first initial number, number1
. If we have a valid initial number then the rawValue
of our InitialNumber
enum is greater than 0 and we can send a success
result to Siri’s completion handler. All is well. If the rawValue
is 0, then Siri wasn’t able to match our spoken input to one of the enum values, and so we send a needsValue
result; this tells Siri to ask the user again for a number.
We can resolve numbers 2-6 in the same way, so I won’t trouble you with the duplication.
Resolving the target is a similar process:
func resolveTarget(for intent: SolveGameIntent, with completion: @escaping (SolveGameTargetResolutionResult) -> Void) {
if let target = intent.target {
completion(SolveGameTargetResolutionResult.success(with: Int(truncating: target)))
} else {
completion(SolveGameTargetResolutionResult.needsValue())
}
}
If the target exists then we can send the success
result. Otherwise our intent needs a value and Siri will ask one more time.
Handling data
We have all our data, and it’s valid - what now? The next stage is to handle the data, perform the calculation, and pass the result, in the form of one or more parameters, to Siri. First, though, we need to define those result parameters. In our app’s target let’s reopen Intents.intentdefinition
. Click on the Response item on the left. We’ll add two strings as our response properties -
howManyAway
which contains a description of how close the solver came to solving the game, andspokenResult
which describes the solution.
If the solving process was successful we can add those parameters to our success
response template, as per the screenshot below.
Let’s make use of those parameters now. Let’s open our Solve Game intent handler and add the following piece of code. It’s not an elegant piece of code, but hey.
func handle(intent: SolveGameIntent, completion: @escaping (SolveGameIntentResponse) -> Void) {
let (closest, result) = Solver.helper.getResult(numbers: [
intent.number1.rawValue,
intent.number2.rawValue,
intent.number3.rawValue,
intent.number4.rawValue,
intent.number5.rawValue,
intent.number6.rawValue
], target: Int(truncating: intent.target!))
let howManyAway = closest == 0 ? "I've found a solution" : "I could only find a solution \(closest) away"
completion(SolveGameIntentResponse.success(howManyAway: howManyAway, spokenResult: result))
}
The principle is the same as before - sending a success
result to Siri’s completion handler when the request is handled. Here we have a Solver class that does all the work in generating a solution, and we pass the howManyAway
and spokenResult
parameters to Siri in our success
result.
Lost in translation
Let’s try it out!
Siri asks for our first number - let’s say 3:
Our first number is recognised…
So far so good. Siri is now asking for a second number - let’s say 8:
…but our second number is not
Eh? Siri is asking us for the second number again. This is a problem - what is going on here and how do we fix it? In Part 6 we’ll journey into the dark heart of Apple’s documentation and attempt to bridge the gaps we find therein.