SiriKit Tutorial for iOS

Learn how to connect your iOS app with Siri in this SiriKit tutorial for iOS so that users can interact with your app with their voice. By Richard Turton.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

99 (passengers in) Red Balloons

Handling an intent is a three-stage process. The first stage is called Resolution. In this stage, your extension has to confirm that all of the information it needs about the intent is present. If there is information missing, Siri can ask the user additional questions.

The information varies depending on the particular intent. For the ride request intent, there are the following parameters:

  • Pickup location
  • Drop-off location
  • Party size
  • Ride option
  • Payment method
Note: If your app isn’t interested in some of the parameters, such as if you only accept Apple Pay for payments, then you can ignore them.

Each parameter comes with a related method in the handler protocol. Remember that you’re using the INRequestRideIntentHandling for handling intents in this app. That protocol has methods for resolving each of the parameters above. Each one receives a ride request intent as a parameter and has a completion block, which you call when you’ve processed the intent. The completion block takes an INIntentResolutionResult subclass as a parameter.

The resolution result tells Siri what to do next, or if everything is OK, it moves on to the next parameter.

That all sounds a little abstract, so here’s a diagram:

SiriKit Tutorial

Open RideRequestHandler.swift and add the following method:

func resolvePickupLocation(forRequestRide intent: INRequestRideIntent, with completion: @escaping (INPlacemarkResolutionResult) -> Void) {
  if let pickup = intent.pickupLocation {
    completion(.success(with: pickup))
  } else {
    completion(.needsValue())
  }
}

This method resolves the pickup location. The completion block takes a INPlacemarkResolutionResult parameter, which is the specific subclass for dealing with location values in the Intents framework. Here you accept any pickup location that arrives with the intent. If there is no pickup location, you tell Siri that a value is required.

Build and run the app, and ask Siri to book you a ride using WenderLoon, giving no extra information.

SiriKit Tutorial

You supplied no pickup information in the original intent, so the resolution method tells Siri to ask for more data. If you then say a location, the resolution method is called again. The resolution method will get called multiple times until you end up with a success or a failure.

However, the handler object is initialized from scratch for each separate interaction with Siri. A different instance of RideRequestHandler deals with each interaction, which means you cannot use any state information on the handler when dealing with intents.

Back in Xcode, add another resolution method, this time for the drop-off location:

func resolveDropOffLocation(forRequestRide intent: INRequestRideIntent, with completion: @escaping (INPlacemarkResolutionResult) -> Void) {
  if let dropOff = intent.dropOffLocation {
    completion(.success(with: dropOff))
  } else {
    completion(.notRequired())
  }
}

Here you’re allowing a ride with no drop-off location to go ahead. This is actually quite sensible, considering you have absolutely no control over where a hot air balloon will take you. If you build and run, Siri will use a drop-off location that you supply, but it won’t try and fill in the gaps if there isn’t one present.

As well as simply accepting any value that’s passed in as an intent parameter, you can also perform a bit of business logic in there. In many cases, this will involve the same logic used in the main app. Apple recommends that you put code such as this in a separate framework that can be shared between your extension and the main app.

That’s why the sample project contains the WenderLoonCore framework. Bring that framework into the extension by adding the following statement to the top of RideRequestHandler.swift:

import WenderLoonCore

Then add the following property and initializer to RideRequestHandler:

let simulator: WenderLoonSimulator

init(simulator: WenderLoonSimulator) {
  self.simulator = simulator
  super.init()
}

WenderLoonSimulator is an object which contains the business logic for the app. Open IntentHandler.swift and add the following to the top of the file:

import WenderLoonCore

let simulator = WenderLoonSimulator(renderer: nil)

Then replace the line where the request handler is created (it will have an error on it) with the following:

return RideRequestHandler(simulator: simulator)

Now your request handler will be able to access the business logic from the rest of the app.

Back in RideRequestHandler.swift, add the following method for resolving the number of passengers:

func resolvePartySize(forRequestRide intent: INRequestRideIntent, with completion: @escaping (INIntegerResolutionResult) -> Void) {
  switch intent.partySize {
  case .none:
    completion(.needsValue())
  case let .some(p) where simulator.checkNumberOfPassengers(p):
    completion(.success(with: p))
  default:
    completion(.unsupported())
  }
}

This will ask for a number of passengers if the intent doesn’t already contain that information. If the number of passengers is known, it is validated against the rules held in the WenderLoonSimulator object. The maximum number of passengers is four. Build and run and see what happens with different party sizes:

SiriKit Tutorial

You’ve seen that the resolution stage works by dealing with a single parameter at a time. In the next stage, you can handle the final intent with all of the parameters resolved.

The Confirmation stage of intent handling happens after all of the parameters have been resolved. As with resolution, there are delegate methods specific to each intent. The delegate method has a similar signature to the resolution methods, but there is only one per intent.

Add the following to RideRequestHandler.swift:

func confirm(requestRide intent: INRequestRideIntent, completion: @escaping (INRequestRideIntentResponse) -> Void) {
  let responseCode: INRequestRideIntentResponseCode
  if let location = intent.pickupLocation?.location,
    simulator.pickupWithinRange(location) {
    responseCode = .ready
  } else {
    responseCode = .failureRequiringAppLaunchNoServiceInArea
  }
  let response = INRequestRideIntentResponse(code: responseCode, userActivity: nil)
  completion(response)
}

Here you use a method from the simulator to check that the pickup location is in range. If not, you fail with the “no service in area“ response code.

Sure, you could have performed this check when resolving the pickup location. But then you wouldn’t have seen any implementation at all! :] You can also use this method to ensure that you had connectivity to your services, so the booking could go ahead. This method is called just before the confirmation dialog is shown to the user.

Try to book a ride with a pickup location more than 50 km away from London, and you’ll receive an error telling you there is no service in the area.

Note: If you don’t live near London, edit WenderLoonCore > WenderLoonSimulator.swift > pickupWithinRange(_:) and add a few more zeros to the radius.

You’ve dealt with the first two phases of a Siri interaction: resolution and confirmation. The final phase is where you actually take that intent and convert it into something actionable.