iOS Hands-Free Calling With Siri and Intents
Think it's too hard to get iOS hands-free calling working with Siri? Let one of our resident iOS wizards Quinton Pryce show you how to get it going with intents! If this piques your interest as an iOS developer, you should definitely check out our current job openings.
SiriKit is a wonderfully complex, but disjointedly documented, feature Apple has provided iOS developers. Though the docs make adding "Hey Siri" commands appear difficult, it actually doesn't amount to much work at all and can be done in under a half hour's time.
To start, we need to pick a particular intent for Siri to handle. Coming from a calling and texting platform like TextNow, it makes sense to start with one or the other, and for the purposes of this how-to, we are going to add Siri for calling.
An object representing a user's intent to use an app (i.e. the user's intent to make a call.)
- def'n by me
Lay the groundwork
- Add Siri to your bundle identifiers capabilities.
- Define the Siri usage description in your info.plist.
- In your app's lifecycle, request authorization to use Siri.
Xcode Project Setup
Now that we have our intent picked out (calling), we begin begin with the Xcode project additions which starts with adding the IntentsExtension.
App extensions can be thought of as standalone frameworks that your app can include to provide it with an interface to other apps and the system. In our case we are using an IntentsExtension to receive requests from Siri.
1) Open up your project, select the target you want to implement Siri for, and click the + icon at the bottom of the screen.
2) Select "Intents Extension", ensure that the "Embed in Application" field has your intended target, then "Activate".
3) Modify the "Supported Intents" to have at the very least INStartCallIntent.
Processing information from Siri
The extension we just created has one class in it, IntentsHandler. This is where the system will call to our extension to handle the intent.
The only things that should be done in this class are asking Siri to confirm information, and sending the intent to the main app. The class is pre-filled by default with the messaging intents which we can nuke and add our INStartCallIntentHandling protocol inheritance.
Asking Siri to confirm information
In order to place a call, the user needs to specify a contact or phone number. Depending on whether the user provides a phone number or a contact's name, we may need to ask Siri to confirm any ambiguity with the user’s request.
It is worth noting that the intent object -- in our case INStartCallIntent -- passed back and forth between Siri and our extension, will inevitably be the same object that gets passed to the main app. The IntentsExtension's only priority is to fill that intent with the information needed to complete the task in the main app. Since your IntentsExtension should be stateless, you can use the intent object to store any values that may optimize information processing (ie. storing a search key or a contact result that may speed the search up after Siri comes back with more information from the user).
We can start by defining the resolveContacts(: ) method. Keep in mind that this resolveContacts(: ) method is specific for calling and not all intents will require you to resolve contacts. That being said, no matter what information you need from the user, you may still need to confirm ambiguity with the user which will follow the same process outlined here.
This method will receive calls from SiriKit every time the user makes a request to your app (ie. "Call Quinton with TextNow" or "Call 555-555-1234 with TextNow").
Resolving a phone number is fairly simple, because it will come back on the INPerson object's personHandle value, but resolving contacts is a little trickier. If you end up not needing to resolve contacts you can skip to the next heading.
To resolve a contact's name (ie. Quinton) you will need to write an implementation of the CNContactStore to search through the user's contacts and match the given name with a contact (example: IntentPersonProvider.swift). At this point we may need to ask Siri to confirm the information because the found contact may have multiple numbers. For details on this and what options there are for resolving contacts and what information you can ask Siri to get from the user, see the IntentHandler implementation method `resolveContacts`.
Sending a confirmed contact's phone number to the main app
After we have confirmed that the information provided by the user can be processed by our main app (ie. we have a valid phone number), we can send that information to our app.
The implementation for sending the intent is very simple. Our IntentHandler, by default contains the handle(: ) implementation.
The only responsibility of this method is to build the response with the provided intent (and in our case, ensure that the INPerson object within it has a phone number).
Receiving the intent in our main app
Depending if we are using a `SceneDelegate` or an `AppDelegate` (or both), we'll need to make sure we can receive the intent and dispatch the phone number to our calling mechanism.
Where to go from here
At this point we have set up our extension, confirmed the information, and sent the data to our app.
You can continue to build this extension out to add more intents which can be done by adding more supported intents to our Xcode project. Whether those items are custom intents or the intents that apple has baked in is up to you, but whatever you decide the same pattern applies: Receive request, confirm information, send to your app.
You can debug both sides of this implementation (confirming the intent in the extension, and handling the intent in the main app.
To debug intent confirmation in the extension
- Install your main target to the simulator/device.
- Select the IntentsExtension as the target and run it.
- When prompted for which app to launch, select your main app.
To debug intent handling in the main app
- All you need to do is run your main app. With the extension already embedded in the app, the extension will compile and install alongside your app.