Search

SiriKit Part 2: Resolve, Confirm, Handle

John Daub

8 min read

Sep 27, 2017

iOS

SiriKit Part 2: Resolve, Confirm, Handle

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.

Running with Siri

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:

The BNRun Workout app.

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.

Resolve, Confirm, Handle

There are three steps involved in an Intent handler:

  1. Resolve the parameters: Help Siri understand what the user provided, including the ability to ask the user for clarification, confirmation or additional information.
  2. Confirm all-the-things: Last chance to ensure all the Intent parameters are as they should be, and that your app can handle the user’s request.
  3. Handle it: do the thing!

When starting a workout, a user could say lots of things:

  • “Jog for 10 miles”
  • “Start a run in BNRun”
  • “Start an open swim”
  • “Go for a jog”

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.

Resolve

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).

Confirm

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.

Handle

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)
  }
}

IntentsSupported

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.

Stop Workout

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!

One More Thing…

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.

John Daub

Author Big Nerd Ranch

John “Hsoi” Daub is a Director of Technology (and former Principal Architect) at Big Nerd Ranch. He’s been an avid computer nerd since childhood, with a special love for Apple platforms. Helping people through their journey is what brings him the greatest satisfaction. If he’s not at the computer, he’s probably at the gym lifting things up and putting them down.

Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Not applicable? Click here to schedule a call.

Stay in Touch WITH Big Nerd Ranch News