In 2018, Apple announced Siri Shortcuts, which allow users to interact with apps through Siri and the Shortcuts app without opening the app directly.
The Intents framework also has support for adding and recording shortcuts inside apps and allows apps to get a list of shortcuts the user has added. However, by default, users can only add and manage their shortcuts in the Shortcuts app.
In this tutorial, we’ll explore the best way to let users add and manage their shortcuts in your app, including making a custom screen listing the different shortcuts available.
If you’d prefer to jump straight to a demo app, feel free to click here to go to the Conclusion. There, you’ll find an Xcode Project containing a sample app that uses this code to provide an example of the shortcuts view controller.
This blog post and code is based on this great example Github Gist by Simon Ljungberg, but introduces major changes such as the way shortcuts are loaded and the delegate system. Any code from the Gist is available under the MIT License from the Gist.
Note: Some sections of this tutorial require Xcode 11 and iOS 13, which are currently in beta, as it relies on new features or frameworks that are not available on previous versions.
No installation or configuration is required to go through this tutorial, as SiriKit and the Intents framework is built into iOS. However, this blog post assumes you have already integrated and set up Intents in your app, as we’ll be focusing on more advanced features.
Before adding a view controller that lets users add, edit or delete your app’s shortcuts, we’ll create a manager object that abstracts some of the more specific APIs to allow the view controller to easily perform actions. I find that avoiding storing the added voice shortcuts to disk is best, as it avoids inconsistency when shortcuts are added or removed in the Shortcuts app.
First, create a class called ShortcutsManager, and use a singleton to avoid having multiple instances.
Then, create a nested enum that will hold all of the different intent types your app supports. For example, a soup ordering app might have an order soup intent. We’ll also use computed variables to specify each intent type’s intent class, which is the class automatically generated by the Intents framework, and a suggested invocation phrase, which will be shown to users when they are adding the shortcut.
Next, add a nested struct that we’ll use to abstract the actual INVoiceShortcut type, so our view controller can display shortcuts with or without them being added by the user.
Thanks to Swift, the struct will automatically get a custom initializer with each of the properties, so we don’t need to create it ourselves.
Before adding a function to load shortcuts, we need to create a small private helper that we’ll use to find intents of the right kind:
Now, we need to make a function that loads all of the available shortcuts and shortcuts the user has added. We’ll create the function in stages, but I’ll add the complete function at the bottom of this section so you can check your work.
First, create the function declaration. Our view controller will provide a list of intent kinds it wants shortcuts for, and we’ll call the completion handler with the shortcuts.
In the function, we’ll call the Intents framework’s getAllVoiceShortcuts function, which will give us all of the shortcuts the user has added to Siri. If it fails or doesn’t return any voice shortcuts, we’ll just call the completion handler with Shortcut objects without the voiceShortcut variable. This way, the view controller can display a list of available shortcuts that haven’t been added.
Then, we’ll go through each of the intent kinds that were passed in and try to find a corresponding voice shortcut. If we can’t find a corresponding voice shortcut, we’ll just return a Shortcut without the voiceShortcut variable, which means the user hasn’t added it to Siri.
As you can see in the complete function, if a shortcut hasn’t been added, we return a Shortcut object without the voiceShortcut variable, and if a shortcut was added, we’ll set the voiceShortcut to the object returned by the system. This way, our view controller can show both added and available shortcuts.
The Intents framework has two built in view controllers that we can present to let the user manage a shortcut: INUIAddVoiceShortcutViewController, to add a new shortcut, and INUIEditVoiceShortcutViewController, to change the invocation phrase for an existing shortcut or delete it.
To abstract their delegates, we’ll create a ShortcutsManagerDelegate and then make a DelegateProxy that calls our delegate when one of the framework’s delegates are called.
First, we’ll make a ShortcutsManagerDelegate, which will have similar methods to the INUIAddVoiceShortcutViewControllerDelegate and INUIEditVoiceShortcutViewControllerDelegate.
Next, we’ll create a private DelegateProxy class, which we’ll use internally in the ShortcutsManager. This class won’t be exposed to the view controller, and is a little lengthy as it conforms to both INUIAddVoiceShortcutViewControllerDelegate and INUIEditVoiceShortcutViewControllerDelegate.
I won’t go over the entire class, but as you can see, it just dismisses each controller and forwards delegate calls to our custom delegate.
Finally, we’ll add an array of delegate proxies and create the function which will be called by our view controller to present the appropriate intents view controller.
In the function, we create a DelegateProxy to abstract the delegates and present the add or edit view controller based on whether the Shortcut has an existing INVoiceShortcut.
Creating a shortcuts view controller
After creating our ShortcutsManager with support for loading and editing shortcuts, we’ll create a simple UITableViewController to list both available and added shortcuts, and allow the user to edit them.
Note: In this section, I’m using UITableViewDiffableDataSource and related APIs, which require Xcode 11 and iOS 13 (currently in beta) to simplify it. If you need to support iOS 12, you can use the older data source APIs instead.
First, create a new view controller that conforms to UITableViewController. We’ll also create a delegate that the view controller conforms to, so the custom data source class we’ll create later can access the shortcuts stored on the view controller.
In the view controller, we’ll add variables for the shortcuts and for our diffable data source:
Then, we’ll create a custom nested Section enum, which will act as the custom type for our table view sections.
Next, we’ll add a custom initializer to setup our data source and load our shortcuts. To satisfy the compiler, we have to add the required init(coder:) initializer, but it won’t be called.
When we’re loading our shortcuts from the ShortcutsManager, we’ll separate them into all shortcuts and shortcuts the user has already added.
Now, we can create a nested custom DataSource class, which will be a subclass of UITableViewDiffableDataSource. We’ll setup each cell with a check or plus SF Symbol, the shortcut’s suggested invocation phrase, and if it’s been added, the custom invocation phrase the user chose.
Finally, we’ll show the add or edit view controller when a row is tapped and make the view controller conform to ShortcutsManagerDelegate.
When conforming to the ShortcutsManagerDelegate, we’re just reloading our shortcuts (which also reloads the data source), and thanks to diffable data sources, the table view will animate automatically.
In this tutorial, we went in depth into creating a custom ShortcutsManager and AllShortcutsViewController to allow users to manage their shortcuts without leaving your app.
For your reference, I’ve created a simple example Xcode Project with a demo app which contains the ShortcutsManager and AllShortcutsViewController from this tutorial.
I hope this tutorial was useful, whether you just wanted to learn more about Siri Shortcuts or are trying to implement your own shortcuts management screen. If you have any questions or feedback, feel free to reach out on Twitter or email me: [email protected].