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 3 of 4 of this article. Click here to view the first page.

You Can’t Handle the Truth

You implemented a handler way back in the first section of the SiriKit tutorial. All it did was return a failure code, saying there was no service in the area. Now, you’re armed with a fully populated intent so you can perform more useful work.

After the user has seen the confirmation dialog and has requested the ride, Siri shows another dialog with the details of the ride that has been booked. The details of this dialog will differ between the different intents, but in each case you must supply certain relevant details. Each intent actually has its own data model subset, so you need to translate the relevant part of your app’s data model to the standardized models used by the Intents framework.

Switch schemes to the WenderLoonCore framework, add a new Swift file to the Extensions group and name it IntentsModels.swift. Replace the contents with the following:

import Intents

// 1
public extension UIImage {
  public var inImage: INImage {
    return INImage(imageData: UIImagePNGRepresentation(self)!)
  }
}

// 2
public extension Driver {
  public var rideIntentDriver: INRideDriver {
    return INRideDriver(
      personHandle: INPersonHandle(value: name, type: .unknown),
      nameComponents: .none,
      displayName: name,
      image: picture.inImage,
      rating: rating.toString,
      phoneNumber: .none)
  }
}

Here’s what each method does:

  1. The Intents framework, for some reason, uses its own image class INImage. This UIImage extension gives you a handy way to create an INImage.
  2. INRideDriver represents a driver in the Intents framework. Here you pass across the relevant values from the Driver object in use in the rest of the app.

Unfortunately there’s no INBalloon. The Intents framework has a boring old INRideVehicle instead. Add this extension to create one:

public extension Balloon {
  public var rideIntentVehicle: INRideVehicle {
    let vehicle = INRideVehicle()
    vehicle.location = location
    vehicle.manufacturer = "Hot Air Balloon"
    vehicle.registrationPlate = "B4LL 00N"
    vehicle.mapAnnotationImage = image.inImage
    return vehicle
  }
}

This creates a vehicle based on the balloon’s properties.

With that bit of model work in place you can build the framework (press Command-B to do that) then switch back to the ride request extension scheme.

Open RideRequestHandler.swift and replace the implementation of handle(intent:completion:) with the following:

// 1
guard let pickup = intent.pickupLocation?.location else {
  let response = INRequestRideIntentResponse(code: .failure,
    userActivity: .none)
  completion(response)
  return
}

// 2
let dropoff = intent.dropOffLocation?.location ??
  pickup.randomPointWithin(radius: 10_000)

// 3
let response: INRequestRideIntentResponse
// 4
if let balloon = simulator.requestRide(pickup: pickup, dropoff: dropoff) {
  // 5
  let status = INRideStatus()
  status.rideIdentifier = balloon.driver.name
  status.phase = .confirmed
  status.vehicle = balloon.rideIntentVehicle
  status.driver = balloon.driver.rideIntentDriver
  status.estimatedPickupDate = balloon.etaAtNextDestination
  status.pickupLocation = intent.pickupLocation
  status.dropOffLocation = intent.dropOffLocation
  
  response = INRequestRideIntentResponse(code: .success, userActivity: .none)
  response.rideStatus = status
} else {
  response = INRequestRideIntentResponse(code: .failureRequiringAppLaunchNoServiceInArea, userActivity: .none)
}

completion(response)

Here’s the breakdown:

  1. Theoretically, it should be impossible to reach this method without having resolved a pickup location, but hey, Siri…
  2. We’ve decided to embrace the randomness of hot air balloons by not forcing a dropoff location, but the balloon simulator still needs somewhere to drift to.
  3. The INRequestRideIntentResponse object will encapsulate all of the information concerning the ride.
  4. This method checks that a balloon is available and within range, and returns it if so. This means the ride booking can go ahead. If not, you return a failure.
  5. INRideStatus contains information about the ride itself. You populate this object with the Intents versions of the app’s model classes. Then, you attach the ride status to the response object and return it.
Note: The values being used here aren’t what you should use in an actual ride booking app. The identifier should be something like a UUID, you’d need to be more specific about the dropoff location, and you’d need to implement the actual booking for your actual drivers :]

Build and run; book a ride for three passengers, pickup somewhere in London, then confirm the request. You’ll see the final screen:

SiriKit Tutorial

Hmmm. That’s quite lovely, but it isn’t very balloon-ish. In the final part, you’ll create custom UI for this stage!

Making a Balloon Animal, er, UI

To make your own UI for Siri, you need to add another extension to the app. Go to File\New\Target… and choose the Intents UI Extension template from the Application Extension group.

Enter LoonUIExtension for the Product Name and click Finish. Activate the scheme if you are prompted to do so. You’ll see a new group in the project navigator, LoonUIExtension.

A UI extension consists of a view controller, a storyboard and an Info.plist file. Open the Info.plist file and, the same as you did with the Intents extension, change the NSExtension/NSExtensionAttributes/IntentsSupported array to contain INRequestRideIntent.

Each Intents UI extension must only contain one view controller, but that view controller can support multiple intents.

Open MainInterface.storyboard. You’re going to do some quick and dirty Interface Builder work here, since the actual layout isn’t super-important.

Drag in an image view, pin it to the top, left and bottom edges of the container and set width to 0.25x the container width. Set the Content Mode to Aspect Fit.

Drag in a second image view and pin it to the top, right and bottom edges of the container and set the same width constraint and Content Mode.

Drag in a label, pin it to the horizontal and vertical center of the view controller and set the font to System Thin 20.0 and the text to WenderLoon.

Drag in another label, positioned the standard distance underneath the first. Set the text to subtitle. Add a constraint for the vertical spacing to the original label and another to pin it to the horizontal center.

Make the background an attractive blue color.

This is what you’re aiming for:

SiriKit Tutorial

Open the assistant editor and create the following outlets:

  • The left image view, called balloonImageView
  • The right image view, called driverImageView
  • The subtitle label, called subtitleLabel

In IntentViewController.swift, import the core app framework:

import WenderLoonCore

You configure the view controller in the configure(with: context: completion:) method. Replace the template code with this:

// 1
guard let response = interaction.intentResponse as? INRequestRideIntentResponse
  else {
    driverImageView.image = nil
    balloonImageView.image = nil
    subtitleLabel.text = ""
    completion?(self.desiredSize)
    return
}

// 2
if let driver = response.rideStatus?.driver {
  let name = driver.displayName
  driverImageView.image = WenderLoonSimulator.imageForDriver(name: name)
  balloonImageView.image = WenderLoonSimulator.imageForBallon(driverName: name)
  subtitleLabel.text = "\(name) will arrive soon!"
} else {
// 3
  driverImageView.image = nil
  balloonImageView.image = nil
  subtitleLabel.text = "Preparing..."
}

// 4
completion?(self.desiredSize)

Here’s the breakdown:

  1. You could receive any of the listed intents that your extension handles at this point, so you must check which type you’re actually getting. This extension only handles a single intent.
  2. The extension will be called twice. Once for the confirmation dialog and once for the final handled dialog. When the request has been handled, a driver will have been assigned, so you can create the appropriate UI.
  3. If the booking is at the confirmation stage, you don’t have as much to present.
  4. Finally, you call the completion block that has been passed in. You can vary the size of your view controller and pass in a calculated size. However, the size must be between the maximum and minimum allowed sizes specified by the extensionContext property. desiredSize is a calculated variable added as part of the template that simply gives you the largest allowed size.

Build and run and request a valid ride. Your new UI appears in the Siri interface at the confirmation and handle stages:
SiriKit Tutorial

Notice that your new stuff is sandwiched in between all of the existing Siri stuff. There isn’t a huge amount you can do about that. If your view controller implements the INUIHostedViewSiriProviding protocol then you can tell Siri not to display maps (which would turn off the map in the confirm step), messages (which only affects extensions in the Messages domain) or payment transactions.