Swift Regex Deep Dive
iOS MacOur introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
Siri is Apple’s intelligent personal assistant. Siri allows you to use your voice to interact with your iOS, watchOS, tvOS and macOS devices. As with many Apple technologies, Apple has made it easier for developers to integrate their apps with Siri through SiriKit. This series explores SiriKit and how you can use it to expose your app’s functionality through Siri. In Part 1, we looked at the basics of SiriKit. Here in Part 2, we’ll look at the heart of SiriKit: Resolve, Confirm, and Handle.
Folks at Big Nerd Ranch like to work out, especially by lifting weights and running. Having an app to keep track of our workouts would be useful, so enter BNRun, and its simple sample code:
In Part 1,I mentioned that Siri is limited it what it can do. When deciding to add Siri support to your app, you have to reconcile your app’s functionality against what SiriKit offers in its Domains and Intents. BNRun is a workout app, and SiriKit offers a Workouts Domain, so that’s a good start. Looking at the Intents within the Workout Domain, there is nothing that lends to sets/reps/weight, but there are Intents that lend to cardio workouts, like a run or a swim. So Siri won’t be able to support everything I want to do, but I will use Siri for what it can do. To keep things simple, I’ll focus on Starting and Stopping workouts.
However, before diving into the Intents framework, I have to step back and look at my code against the Intents. Every Intent has different requirements: some are simple and self-contained, others require support from the app and some must have the app do the heavy lifting. It’s essential to read Apple’s documentation on the Intent to know how it can and must be implemented, because this affects how you approach not just your Intent, but your application.
In BNRun and its chosen Intents, the app itself must take care of the heavy lifting. However, the Intents must have some knowledge and ability to work with the app’s data model. As a result, the app’s data model must be refactored into an embedded framework so it can be shared between the app and the extension. You can see this refactoring in phase 2 of the sample code. It’s beyond the scope of this article to talk about embedded frameworks. Just know that an Intents Extension is an app extension and thus prescribes to the features, limitations and requirements of app extensions; this can include using embedded frameworks, app groups, etc. to enable sharing of code and data between your app and your extension.
There are three steps involved in an Intent handler:
When starting a workout, a user could say lots of things:
Siri takes the user’s natural language input, converts it to text, and does the work to determine what the user wants to do. When Siri determines the user wants to do something involving your app, your Intents Extension is loaded. The OS examines the extension’s Info.plist
looking for the NSExtensionPrincipalClass
as the entry point into the extension. This class must be a subclass of INExtension
, and must implement the INIntentHandlerProviding
function: handler(for intent: INIntent) -> Any?
returning the instance of the handler that will process the user’s command. In a simple implementation where the principal class implements the full handler, it might look something like this:
import Intents
class IntentHandler: INExtension /* list of `Handling` protocols conformed to */ {
override func handler(for intent: INIntent) -> Any? {
return self
}
// implement resolution functions
}
While I could implement the whole of my extension within the principal class, by factoring my handlers into their own classes and files, I’ll be better positioned for expanding the functionality of my extension (as you’ll see below). Thus, for the Start Workout Intent, I’ll implement the principal class like this:
import Intents
class IntentHandler: INExtension {
override func handler(for intent: INIntent) -> Any? {
if intent is INStartWorkoutIntent {
return StartWorkoutIntentHandler()
}
return nil
}
}
StartWorkoutIntentHandler
is an NSObject
-based subclass that implements the INStartWorkoutIntentHandling
protocol, allowing it to handle the Start Workout Intent. If you look at the declaration of INStartWorkoutIntentHandling
, you’ll see one only needs to handle
the Intent (required by the protocol): one doesn’t need to resolve
nor confirm
(optional protocol requirements). However, as there are lots of ways a user could start a workout but my app only supports a few ways, I’m going to have to resolve and confirm the parameters.
BNRun supports three types of workouts: walking, running and swimming. The Start Workout Intent doesn’t support a notion of a workout type, but it does support a notion of a workout name. I can use the workout name as the means of limiting the user to walking, running and swimming. This is done by implementing the resolveWorkoutName(for intent:, with completion:)
function:
func resolveWorkoutName(for intent: INStartWorkoutIntent,
with completion: @escaping (INSpeakableStringResolutionResult) -> Void) {
let result: INSpeakableStringResolutionResult
if let workoutName = intent.workoutName {
if let workoutType = Workout.WorkoutType(intentWorkoutName: workoutName) {
result = INSpeakableStringResolutionResult.success(with: workoutType.speakableString)
}
else {
let possibleNames = [
Workout.WorkoutType.walk.speakableString,
Workout.WorkoutType.run.speakableString,
Workout.WorkoutType.swim.speakableString
]
result = INSpeakableStringResolutionResult.disambiguation(with: possibleNames)
}
}
else {
result = INSpeakableStringResolutionResult.needsValue()
}
completion(result)
}
The purpose of the resolve
functions is to resolve parameters. Is the parameter required? Optional? Unclear and needs further input from the user? The implementation of the resolve
functions should examine the data provided by the given Intent, including the possibility the parameter wasn’t provided. Depending upon the Intent data, create a INIntentResolutionResult
to let Siri know how the parameter was resolved. Actually, you create an instance of the specific INIntentResolutionResult
type appropriate for the resolve—in this case, a INSpeakableStringResolutionResult
(the type of result will be given in the resolve
function’s signature).
All results can respond as needing a value, optional, or that this parameter is unsupported. Specific result types might add more contextually appropriate results. For example, with INSpeakableStringResolutionResult
, a result could be success with the name; or if a name was provided but it wasn’t one the app understood, a list is provided to the user. Every result type is different, so check documentation to know what you can return and what it means to return that type. Don’t be afraid to experiment with the different results to see how Siri voices the result to the user.
Important Note! Before exiting any of the three types of Intent-handler functions, you must invoke the completion closure passing your result. Siri cannot proceed until the completion is invoked. Ensure all code paths end with the completion (consider taking advantage of Swift’s defer
).
Once parameters have been resolved, it’s time to confirm the user’s intent can go forward. If BNRun connected to a server, this might be the time to ensure such a connection could occur. In this simple sample, it’s only important to ensure that a Workout
can be constructed from the INStartWorkoutIntent
.
func confirm(intent: INStartWorkoutIntent,
completion: @escaping (INStartWorkoutIntentResponse) -> Void) {
let response: INStartWorkoutIntentResponse
if let workout = Workout(startWorkoutIntent: intent) {
if #available(iOS 11, *) {
response = INStartWorkoutIntentResponse(code: .ready, userActivity: nil)
}
else {
let userActivity = NSUserActivity(bnrActivity: .startWorkout(workout))
response = INStartWorkoutIntentResponse(code: .ready, userActivity: userActivity)
}
}
else {
response = INStartWorkoutIntentResponse(code: .failure, userActivity: nil)
}
completion(response)
}
Notice the use of #available
? iOS 11 changed how the Workouts Domain interacts with the app, providing a better means of launching the app in the background. Check out the WWDC 2017 Session 214 “What’s New In SiriKit” for more information.
Handling the user’s intent is typically the only required aspect of an Intent handler.
func handle(intent: INStartWorkoutIntent,
completion: @escaping (INStartWorkoutIntentResponse) -> Void) {
let response: INStartWorkoutIntentResponse
if #available(iOS 11, *) {
response = INStartWorkoutIntentResponse(code: .handleInApp, userActivity: nil)
}
else {
if let workout = Workout(startWorkoutIntent: intent) {
let userActivity = NSUserActivity(bnrActivity: .startWorkout(workout))
response = INStartWorkoutIntentResponse(code: .continueInApp, userActivity: userActivity)
}
else {
response = INStartWorkoutIntentResponse(code: .failure, userActivity: nil)
}
}
completion(response)
}
While some Intents can handle things within the extension, a workout must be started within the app itself. The iOS 10 way required the creation of an NSUserActivity
, implementing the UIApplicationDelegate
function application(_:, continue userActivity:, restorationHandler:)
just like supporting Handoff. While this works, iOS 11 introduces application(_:, handle intent:, completionHandler:)
to UIApplicationDelegate
that more cleanly handles the Intent. Again, see the WWDC 2017 Session 214 “What’s New In SiriKit” for more information.
class AppDelegate: UIResponder, UIApplicationDelegate {
@available(iOS 11.0, *)
func application(_ application: UIApplication, handle intent: INIntent,
completionHandler: @escaping (INIntentResponse) -> Void) {
let response: INIntentResponse
if let startIntent = intent as? INStartWorkoutIntent,
let workout = Workout(startWorkoutIntent: startIntent) {
var log = WorkoutLog.load()
log.start(workout: workout)
response = INStartWorkoutIntentResponse(code: .success, userActivity: nil)
}
else {
response = INStartWorkoutIntentResponse(code: .failure, userActivity: nil)
}
completionHandler(response)
}
}
With the extension now implementing the three steps of resolve, confirm, and handle, the Intent handler is complete. Now the OS needs to know the Intent exists. Edit the extension’s Info.plist
and add the INStartWorkoutIntent
to the IntentsSupported
dictionary of the NSExtensionAttributes
of the NSExtension
dictionary.
To see how this all comes together, take a look at phase 3 of the sample code.
Since the app supports starting a workout, it should also support stopping a workout. Phase 4 of the sample code adds a StopWorkoutIntentHandler
. The IntentHandler
adds a case for it. StopWorkoutIntentHandler
is implemented, providing confirm
and handle
steps (there are no parameters to resolve
in BNRun). And the Info.plist
appropriately lists the intent.
You should be able to build and run the Phase 4 code, starting and stopping workouts within the app, within Siri, or a combination of the two. Give it a try!
Implementing the resolve, confirm, and handle functions takes care of the heavy lifting required for an app to work with Siri. But before shipping your awesome Siri-enabled app to the world, there are a few more things that need to be done. Those things will be covered in more detail in Part 3.
And if you’re having trouble implementing SiriKit or other features into your iOS or watchOS app, Big Nerd Ranch is happy to help. Get in touch to see how our team can build a Siri-enabled app for you, or implement new features into an existing app.
Our introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
The Combine framework in Swift is a powerful declarative API for the asynchronous processing of values over time. It takes full advantage of Swift...
SwiftUI has changed a great many things about how developers create applications for iOS, and not just in the way we lay out our...