It’s new Apple’s feature for iOS 12, announced during the WWDC 2018 keynote and it is coming this fall… so better get your applications ready so your users can take advantage of them as soon as possible. I am quite sure they will appreciate that since it is indeed really a handful and can make life easier — especially if you add shortcuts that will be used often and it all can be done directly from the lock screen, in Search, or from the Siri watch face.
Shortcuts can be suggested by Siri using a signal like location, time of day and type of motion (such as walking, running or driving) and appear on the lock screen as notifications which user can tap to run the task. Do not worry about your privacy — all collected data is stored on your device so nothing will be compromised.
It’ll be possible to add shortcuts from the gallery of samples which will be available in the new Shortcuts app once it’s officially released or Developers can create their custom shortcuts and provide them together with their applications. In this story I’ll show you how to create simple custom shortcut — just remember it’s still in beta so implementation might change a little bit once it’s officially released — if anything changes I’ll try to keep it up to date.
I have prepared a simple project which you can find on GitHub. Feel free to give it a try if you want to skip all the coding and just try it out.
The first thing you have to do (assuming you already have an [empty] project) — add a new target and as template choose Cocoa Touch Framework
.
We will use this target as a container for our API to fetch pieces of information from GitHub and framework will help us to share the code between application and and our shortcut (intent).
Start with creating a new class which will be used as a model for fetched data (user and its followers)
public struct GitHubUser: Codable {
public let name: String
public let location: String
public let repos: Int
private enum CodingKeys: String, CodingKey {
case name
case location
case repos = "public_repos"
}
}
public struct GitHubFollower: Codable {
public let login: String
private enum CodingKeys: String, CodingKey {
case login
}
}
I have split it into two structs because we have to fetch them from two different endpoints and such an approach just makes it faster. Should use a better structure for a production code — I try to focus on creating a shortcut and writing just something “quick” for anything else — keep it in mind ☺️
Now let’s create an API to fetch all data we need
public final class Fetcher: NSObject {
public static func fetch(name: String, completion: @escaping ((user: GitHubUser?, followers: [GitHubFollower])) -> Void) {
guard let url = URL(string: "https://api.github.com/users/\(name)") else {
return
}
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 10.0)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard
let data = data,
let user = try? JSONDecoder().decode(GitHubUser.self, from: data)
else {
completion((nil, []))
return
}
self.fetchFollowers(for: name, completion: { followers in
completion((user, followers))
})
}
task.resume()
}
private static func fetchFollowers(for name: String, completion: @escaping (_ followers: [GitHubFollower]) -> Void) {
guard let url = URL(string: "https://api.github.com/users/\(name)/followers") else {
return
}
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 10.0)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard
let data = data,
let followers = try? JSONDecoder().decode([GitHubFollower].self, from: data)
else {
completion([])
return
}
completion(followers)
}
task.resume()
}
}
I am not explaining above code since I assume you are either familiar with URLSession
or you will just get your data from somewhere else, it can even be a static method.
Now create a New File — SiriKit Intent Definition
Opening it will show a simple editor where we can define custom (or modify system’s) intents and it’s response. Press a ➕ symbol to add new intent.
Let me explain what happened here 🔥
As a category choose whatever fits your intent the best — I have chosen Do. Title and Description are self-explanatory. You can also choose a default image if you feel like so and if you want your intent to be confirmed before executed you can check the mark.
In Parameters
section define all parameters that can be passed to your intent — for me it’s just a GitHub user’s name
. You can have no parameters if you want to.
Now let’s define a Shortcut Type
. Again press a ➕ symbol under the 3rd section, check your parameter if you created one and click Add Shortcut Type
— give it a title and here you can use your parameter which will be replaced with the given string while donating intent (will explain intent donation process later). In my example, I also checked background execution since I want it to be executed without having to run the actual application.
It’s time to define an answer
It is quite simple as well — list of properties
that can be passed to a response and response templates
. Might look complicated for now but once we start writing code it will be explained.
Now you might wonder — how can we access those intents? — similar to CoreData
all needed classes will be automatically generated for us, so no worry about that. Just one thing we have to do here to make sure all works properly since we created a framework to share between targets — look at the image below — it’s Target Membership for a intentdefinition
file we just created. We have to generate classes only for our framework and skip generating for any other target.
So… we have done so much work for now and there is still no results? Just a few more steps and we’ll be done with a basic shortcut.
Add new target — Intent Extension
— you can include UI Extension
as well if you want to, it’s optional but will optionally need it later on.
Remember to link the framework we created earlier with our new target(s).
As soon as we create intent target there will be a generated file IntentHandler.swift, let’s open it and replace its implementation to return handler for a custom intent we defined.
class IntentHandler: INExtension {
override func handler(for intent: INIntent) -> Any {
guard intent is CheckMyGitHubIntent else {
fatalError("Unhandled intent type: \(intent)")
}
return CheckMyGitHubIntentHandler()
}
}
Uhm, of course, it won’t compile now since we didn’t create that Handler yet, let’s do it now then 😎
final class CheckMyGitHubIntentHandler: NSObject, CheckMyGitHubIntentHandling {
func handle(intent: CheckMyGitHubIntent, completion: @escaping (CheckMyGitHubIntentResponse) -> Void) {
guard let name = intent.name else {
completion(CheckMyGitHubIntentResponse(code: .failure, userActivity: nil))
return
}
Fetcher.fetch(name: name) { (user, followers) in
guard let user = user else {
completion(CheckMyGitHubIntentResponse(code: .failure, userActivity: nil))
return
}
completion(CheckMyGitHubIntentResponse.success(repos: user.repos as NSNumber, followers: followers.count as NSNumber))
}
}
}
What does it do? It takes a name from the intent and fetches data for that name and calls a completion block with the answer with defined earlier. If you do not want to fetch any data from GitHub you can just pass some static parameters here in the completion blocks.
This one is the easiest. Create a new target for Intent UI if you have not included it earlier. We can create a custom view for a response to our shortcut and configure it here. It will be shown on the Siri screen.
class IntentViewController: UIViewController, INUIHostedViewControlling {
@IBOutlet weak var reposLabel: UILabel!
@IBOutlet weak var followersLabel: UILabel!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
// MARK: - INUIHostedViewControlling
// Prepare your view controller for the interaction to handle.
func configureView(for parameters: Set<INParameter>, of interaction: INInteraction, interactiveBehavior: INUIInteractiveBehavior, context: INUIHostedViewContext, completion: @escaping (Bool, Set<INParameter>, CGSize) -> Void) {
guard
let intent = interaction.intent as? CheckMyGitHubIntent,
let name = intent.name
else {
return
}
activityIndicator.isHidden = false
activityIndicator.startAnimating()
Fetcher.fetch(name: name) { [weak self] user, followers in
guard let user = user else {
self?.hideActivityIndicator()
return
}
DispatchQueue.main.async {
self?.reposLabel.text = "Repos: \(user.repos)"
self?.followersLabel.text = "Followers: \(followers.count)"
self?.hideActivityIndicator()
}
}
completion(true, parameters, self.desiredSize)
}
var desiredSize: CGSize {
var size = self.extensionContext!.hostedViewMaximumAllowedSize
size.height = UIFont.systemFont(ofSize: 15).lineHeight * 3
return size
}
private func hideActivityIndicator() {
DispatchQueue.main.async {
self.activityIndicator.isHidden = true
self.activityIndicator.stopAnimating()
}
}
}
I have created a simple view with a text field where I can type in a GitHub user’s name and a button that will donate intent
private func donate(name: String) {
// 1
let intent = CheckMyGitHubIntent()
// 2
intent.suggestedInvocationPhrase = "Check my GitHub"
intent.name = name
// 3
let interaction = INInteraction(intent: intent, response: nil)
// 4
interaction.donate { (error) in
if error != nil {
if let error = error as NSError? {
print("Interaction donation failed: \(error.description)")
} else {
print("Successfully donated interaction")
}
}
}
}
INPreferences.requestSiriAuthorization
prior to donating might be required when running on an actual device.
Pretty much… start our app and trigger the intent donation. Now go to system’s Settings -> Siri and add the shortcut for an intent we just donated (if it was donated successfully we should have it available here).
And… we are ready to check everything we have done in action.
There is way more stuff we can do with the new feature — like attaching a user activity that can be used as a shortcut as well. Might cover it in another story or update this one — depending on what readers would rather prefer to see.