On the WWDC conference in 2016, Apple announced SiriKit, which enables developers to provide functionality that can be executed directly from Siri, without opening the app. This is just another step to the idea of using new, innovative ways to interact with the users by using conversational interfaces, simplifying the whole user experience. Your app can now provide functionality to Siri directly from the lock screen and when the app is not even started. However, as it’s usually the case with Apple, there are some limitations. You can use SiriKit only for certain predefined domains, check the Siri programming guide for reference:
– VoIP calling – Messaging – Payments
– Ride booking
– CarPlay (automotive vendors only)
– Restaurant reservations (requires additional support from Apple).
So if your app is not solving problems in one of those domains, you will need to wait (or even suggest to Apple) for an extension in the domain that your app needs. In this post, we will look at the “Ride booking” domain. We will build a simple app that will reserve (fake) ride between the two locations provided by the user. So let’s get started!
First, we need to build the mobile app, which will provide the base booking a ride functionality. The UI will be pretty simple, just two text fields for the ‘from’ and ‘to’ location, and a button that will invoke the booking. When the button is clicked, the found rides are displayed in a list below it.
The scope of this tutorial will be exploring the SiriKit functionalities, we will not be connecting to a real booking a ride service – that would be too much for one tutorial. If you want to connect to such API, depending on the country you want to support, there are few options, like Uber’s or Ola’s APIs. Instead, we will just create a list of dummy rides that we will offer to the users. First, let’s start with a protocol:
This method defines what we should expect from ‘booking a ride’ service – finding list of possible rides, depending on the starting and ending location. The Ride object will be a simple struct, which will contain basic ride information, like the company, car number, the expected time in minutes when the car can pick you up, the type of the ride (whether it’s a limo or taxi), and of course, how much it would cost you.
Then, we will create a dummy implementation of the RideService protocol, which will return few hardcoded rides:
In the view controller, we only need to implement the @IBAction of the button for finding a route, which will ask the dummy service for the rides and the table view data source methods, which will present those rides:
That’s it with the main app. As you see, it’s nothing special, just presenting the found rides. Now let’s start with the interesting part, adding a Siri extension. First, you need to add the Siri capability in the ‘Capabilites’ section for the main app target. This will automatically create an entitlements file with the ‘Siri’ key set to YES. In order to run it on a device, you will need to create an app id which supports Siri and a provisioning profile for that app id. If that’s all setup, you should have all the ticks checked:
The usage of Siri in the app should be transparent to the user – that’s why you need to provide a usage description in the Info.plist file. The key is ‘Privacy – Siri Usage Description’ and for value, we will put something like ‘Siri needed for booking a ride.’ Next, when the screen is shown, we need to check whether the user authorized siri. This can be done using the INPreferences class, from the Intents framework (which we will explore later in greater details):
The Siri extension is created by adding new target called Intents extension (the process is similar to adding other types of extensions, like Watch apps and keyboards extensions). In the process of creating it, you are also asked whether you want to have Intent UI extension, which is used in case you want to modify the default UI displayed in Siri when your app is called.
After the targets are created, you need to provide in their corresponding Info.plist files which extension points do you want to support in your app. The INRequestRideIntent is used for requesting a ride and the INGetRideStatusIntent is used for checking the status of a ride, which is what we would need in our app. The INListRideOptionsIntent is used if you want to provide an “Ride booking” extension from the Maps app, not Siri.
The extension has its “principal class”, which basically implements the required protocol methods, depending on which intents are supported in your app. For example, for the “Ride booking” feature, you need to conform to INRequestRideIntentHandling and INGetRideStatusIntentHandling protocol methods. The next image taken from Apple’s documentation shows the process of implementing the principal class.
The first step is “Resolve” – where you have to determine whether you can handle the parameters provided by the intent (Siri). The second step is “Confirm”, where you are confirming to Siri that you can handle the intent, by providing Response which contains the details displayed in the Siri popup. The response can be different, depending on what kind of intent you are handling, but they all inherit from the base INIntentResponse. For the “Ride booking” use case, you need to return INRequestRideIntentResponse, which contains response code and the ride status. The last step is the handling of the action, where you actually perform the action you’ve confirmed in the previous step.
This is the basic overview of the process, although there are additional optional methods you can implement if you want to improve the user experience in the ride booking process.
Our principal class would be the IntentHandler class, which will conform to the INRequestRideIntentHandling and INGetRideStatusIntentHandling protocols that need to be implemented for the intents we’ve registered.
When the user says something like ‘Hey Siri, book me a ride’, our app will be listed as an option. If the user says ‘book me a ride using BookMeARide’, then our app will directly be asked to perform this intent. Good practice with conversational user interfaces is to request one information at a time, and not proceed to the next step until you get that information. SiriKit follows a similar approach with the resolvePickupLocation and resolveDropOffLocation for the ride intent. If the user for example says “Get me a ride to Paris”, you should handle the missing pickup location and provide INPlacemarkResolutionResult with the needsValue() option. This will tell Siri to ask the user for the pickup location. The same applies for the drop off location (“Get me a ride from London”):
Next, when we have the starting and ending location, we also need to ask for the type of ride the user wants – taxi or a limo. For this, we will implement the resolveRideOptionName. If the user hasn’t specified the type of ride they want, we will resolve the result with a disambiguation and Siri will present the user the two options we support.
Now that we have everything we need, we will need to implement the confirmation of the ride – the check whether we can execute the user’s intent:
First, we are creating the response object (INRequestRideIntentResponse), with a user activity and status code. The user activity is needed in cases when the user opens the app from Siri (this usually happens when an error occured). In the user activity, you can pass some context and prepare your app to proceed at the place where the error with Siri occured. You can see how to access the user activity from the didFinishLaunchingWithOptions method here. The status code supports many options, like success, ready, in progress and few failure codes:
We will assume that it’s success at the beginning and if there’s an error, we will update it accordingly. Next, we will read the pickup and dropOff locations for the ride and the ride option name and based on that decide on the ride type. Then, we ask our ride service (in this case the dummy ride service) to give us a list of possible rides for this route. If there are rides available, we will create a ride status based on the provided information (we will get back on this method shortly). Next, we will save the ride to our RideStorage (we will need this for getting the status of the ride) and then return the completion handler with a successful response. In any other case, we will complete the handler with a failure code.
Now, let’s see how we can create the ride status. The convertRidesToRideStatus method does that:
We first check whether there’s a ride with the provided ride type. If there’s no such ride, we will provide the first option to the user (which is a different ride type, but the user might still be interested). Then, we are reading the price, the currency and the estimated time. This information will be needed for the text for the ride option that will be read to the user by Siri. This is represented by the INRideOption object, which is part of the ride status.
If the user wants to book the ride that Siri provided via our app, the handle method is called, which is actually performing the ride reservation. This is the place where you will again call a REST service with a request to reserve the ride, but since we are working with dummy rides, we will just save to our RideStorage that the ride is confirmed. We will also create ride status with confirmed ride phase. This will tell Siri to present a confirmation screen:
After the users have confirmed the ride, they might be interested to check the status of the ride. One phrase that supports this is ‘How far is my ride using BookMeARide’:
Let’s see how we can implement this. First, we’ve mentioned the RideStorage class. This is just a wrapper around UserDefaults and it has methods to save and remove a ride (we are supporting only one ride at a time), and also returns the latest ride:
When the user asks a phrase which is recognized as part of the INGetRideStatusIntent domain, the following method is called:
What happens here? We are checking whether there’s a ride available in our RideStorage and whether there’s a positive status key for that ride. If there’s not, we will provide a response to the user that we currently don’t have any rides available. Otherwise, we will take the estimated pickup date, and compare it with the current date. Based on the difference, the status of the ride will have different value. The ridePhaseMethod does that:
If the date difference is less than zero, then the car should’ve already arrived, which is why we return the ongoing ride phase. If the car is 60 or more seconds away, we will return the approachingPickup phase and if less than that, the pickup phase, which means that the car is very close and ready.
That’s all we need to do to check the status of the ride. If you’ve noticed on the screenshots, there’s also a little branding part in the Siri screens of our BookMeARide app.
This is done by creating the SiriUI target. In this target, you have a IntentViewController, which provides a way to configure the size of the views that will be presented in the space reserved for custom UI in the Siri popup. There’s also a storyboard file, where you can put images and text, just like in iOS apps. We’ve put a logo and a punchline for our awesome app! One restriction here is that you cannot pass the data from the IntentHandler to this view (at least I haven’t found a way – if you did, please share), so the data presented here is mostly static. You cannot for example create a list of all available rides and put them in a table view in the suplementary section of the popup.
That’s all for this post, I hope you’ve found it interesting and useful for your future endeavors with Siri. You can get the full source code on GitHub.