iOS 12 Siri Shortcuts SiriKit

Filed Under: iOS

In this tutorial, we’ll be discussing how to use Siri capabilities in iOS Apps. We’ll be creating our own ios application shortcut through which Siri would tell us the Latest tutorial released on Journaldev.

SiriKit

Siri is the popular personal assistant found in Apple devices. It helps us and communicates our requirements with the applications. If the applications support the feature they execute it.
This is in layman terms.

For developers, Apple has introduced Sirikit toolkit to allow implementing your app features with Siri.
Sirikit for developers was introduced with iOS 10 with the following Domains/Categories allowed:

  • Messaging
  • VoIP calling
  • Payment
  • Ride booking
  • Photo search
  • Workouts

With the introduction of iOS 12, Siri Shortcuts have been exposed which allow providing key capabilities of your application to Siri. You can use the Shortcuts by saying the respective phrase to Siri.

Siri also predicts the right time to show you a shortcut on your lock screen. Clicking the shortcut handles the Intent defined for that shortcut. Applications can also provide their own custom UI which would be shown by Siri.

How does Siri adopt the Shortcuts defined by the Application?
You need to donate the Shortcuts either using NSUserActivity or using Intents.

Intents use INInteraction class to donate shortcuts.

In this tutorial, we’ll focus on donating shortcuts using Intents.

Intents

Intents are a framework used to define the type of requests and handle the responses from Siri.

To create shortcuts you need to use the following:

  • Intents App Extension – These receive the requests from SiriKit and perform the actions.
  • Intents UI Extension – These are optional if you want to create a custom UI that would be displayed in Siri.
Intents do the following three things

Resolving the voice message – Based on the voice message from the user, it fetches the important parameters that are required for running the Shortcut.

Confirming – The user can confirm the request one last time (provided a confirmation is set in the code).

Handling – Handling the intent i.e. matching the type of the intent from the ones defined and executing.

The above concepts are easier when explained through code.
Let’s start by creating a new XCode project.

We’ll be creating a Siri Shortcut that :

  • Fetches the latest article posted on Journaldev.com from the APIs defined below.
  • Show the user with a custom UI containing the author’s name.
  • Let the user tap on it to open the article link in the web browser.

https://www.journaldev.com/wp-json/wp/v2/posts?filter[posts_per_page]=1&page=1

Getting Started

Following is the gist to create a Siri Shortcut:

  • Add intents and intentsUI extension targets (Targets because the Intents would run outside the app independently).
  • Override the Intent handlers

  • Mention your intents in info.plist

Create a new XCode project as Single View Application.

Enable Siri Capabilities:

ios siri capabilities

Note: To enable Siri capabilities you must have an Apple Developer Account.

Once Siri capabilities is enabled, an entitlements file is created.

Create a custom intent

We need to create a custom intent file in order to perform our specific request with Siri.
Add the following file:

ios siri definition file

Defining Intents

Goto File | New | Target

ios add intents extension

We’d checked the Intents UI to create the target for the same.

Defining the custom intent

ios custom intent and response

  • Creating a new intent named LatestArticle by clicking on the plus symbol.
  • The title would be shown in Siri. We’ve kept the confirmation dialog unchecked for now. Background execution is enabled in order to run the intents in the background.
  • We’ve skipped the Parameters section since our intent does not have any.
  • The response template has two codes. Each with a string text that would be displayed in the Siri.

Make sure that all the three targets are selected.

Adding the Intent to the info.plist.

Make sure the Intent is added to all the three info.plist files as shown below:

ios info plist siri shortcuts

Adding Alamofire Library

Let’s add the Alamofire pod dependency along with SwiftyJSON in our Xcode project from the terminal.
Goto the XCode project path and run the following commands

pod init
open -a Xcode Podfile

Add the pod dependencies in the Podfile at the top:


# Uncomment the next line to define a global platform for your project
platform :ios, '9.0'

pod 'Alamofire'
pod 'SwiftyJSON'
target 'IntentsTest' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for IntentsTest

end

target 'IntentsTestUI' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for IntentsTestUI

end

target 'SiriTest' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!
  # Pods for SiriTest

end

Save the file and exit

pod install

Please close Xcode sessions and use .xcworkspace for opening this project from now on.

Project Structure

ios siri shortcuts project

Code

Create a new Swift file APIController.swift and make sure all the targets are checked.


import Foundation
import Alamofire
import SwiftyJSON

struct APIController {
    
    static var postId = ""
    static var authorAPI = ""
    
    
    func articleOfTheDay(completion: @escaping (String?) -> Void) {
        
        Alamofire.request("https://www.journaldev.com/wp-json/wp/v2/posts?filter[posts_per_page]=1&page=1").responseJSON { response in
            if let result = response.result.value {
                
                let json = JSON(result)
                let str = json.first?.1["title"]["rendered"].stringValue
                let id = json.first?.1["id"].stringValue
                let authorAPI = json.first?.1["_links"]["author"].first?.1["href"].stringValue
            
                APIController.postId = id ?? ""
                APIController.authorAPI = authorAPI ?? ""
                
                completion(str)
            }
        }
        
    }
    
    func contentOfArticle(completion: @escaping (String?) -> Void) {
        Alamofire.request(APIController.authorAPI).responseJSON { response in
            if let result = response.result.value {
                
                let json = JSON(result)
                let str = "Author Name: \(json["name"].stringValue).\nTap to view article"
                
                completion(str)
            }
        }
        
    }
}

We’ve said the authorAPI and post id in static variables.

Let’s look at the Main.storyboard of the application :

ios main storyboard

It’s an empty view controller embedded in a Navigation View Controller.

Inside the ViewController.swift, we’ll add the configuration to donate the Siri Shortcut for the Intent defined earlier. We’ll also add an Add to Siri button to create a shortcut directly from the app itself.

Note: The intentdefinition file auto generates a Swift class when you rebuild the project.


import UIKit
import Intents
import os.log
import Foundation
import IntentsUI
import Alamofire

class ViewController: UIViewController, INUIAddVoiceShortcutViewControllerDelegate, INUIEditVoiceShortcutViewControllerDelegate {
    func editVoiceShortcutViewController(_ controller: INUIEditVoiceShortcutViewController, didUpdate voiceShortcut: INVoiceShortcut?, error: Error?) {
        controller.dismiss(animated: true, completion: nil)
    }
    
    func editVoiceShortcutViewController(_ controller: INUIEditVoiceShortcutViewController, didDeleteVoiceShortcutWithIdentifier deletedVoiceShortcutIdentifier: UUID) {
        controller.dismiss(animated: true, completion: nil)
    }
    
    func editVoiceShortcutViewControllerDidCancel(_ controller: INUIEditVoiceShortcutViewController) {
        controller.dismiss(animated: true, completion: nil)
    }
    
    func addVoiceShortcutViewController(_ controller: INUIAddVoiceShortcutViewController, didFinishWith voiceShortcut: INVoiceShortcut?, error: Error?) {
        controller.dismiss(animated: true, completion: nil)
    }
    
    func addVoiceShortcutViewControllerDidCancel(_ controller: INUIAddVoiceShortcutViewController) {
        controller.dismiss(animated: true, completion: nil)
    }

    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        addSiriButton(to: view)
        
        let apiController = APIController()
        apiController.articleOfTheDay { (articleInfo) in
            if let title = articleInfo {
                self.updateUI(with: title)
            }
        }
        
        donateInteraction()
    }
    
    func donateInteraction() {
        
        let intent = LatestArticleIntent()
        
        intent.suggestedInvocationPhrase = "Lookup JD"
        
        let interaction = INInteraction(intent: intent, response: nil)
        
        interaction.donate { (error) in
            if error != nil {
                if let error = error as NSError? {
                    os_log("Interaction donation failed: %@", log: OSLog.default, type: .error, error)
                } else {
                    print("Successfully donated interaction")
                }
            }
        }
    }
    
    func updateUI(with titleInfo: String) {
        self.title = titleInfo
    }
    
    func addSiriButton(to view: UIView) {
        let button = INUIAddVoiceShortcutButton(style: .blackOutline)
        button.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(button)
        view.centerXAnchor.constraint(equalTo: button.centerXAnchor).isActive = true
        view.centerYAnchor.constraint(equalTo: button.centerYAnchor).isActive = true
        
        button.addTarget(self, action: #selector(addToSiri(_:)), for: .touchUpInside)
    }
    
    @objc
    func addToSiri(_ sender: Any) {
        
        let intent = LatestArticleIntent()
        
        intent.suggestedInvocationPhrase = "Lookup JD"
        if let shortcut = INShortcut(intent: intent) {
            let viewController = INUIAddVoiceShortcutViewController(shortcut: shortcut)
            viewController.modalPresentationStyle = .formSheet
            viewController.delegate = self // Object conforming to `INUIAddVoiceShortcutViewControllerDelegate`.
            present(viewController, animated: true, completion: nil)
        }
    }
    
}

addSiriButton is used to add the UI for the built-in Siri Button.
suggestedInvocationPhrase shows the suggested voice command for the shortcut. Though the user can use any.
donateInteraction function is where we donate the intent to Siri. So you can create the shortcut from the Settings | Siri and Search too.

The above code also updates the title of the ViewController with the latest article retrieved from the API call in the method updateUI.

Now let’s goto the JDIntents folder | IntentHandler.swift:


import Intents

class IntentHandler: INExtension {
    
    override func handler(for intent: INIntent) -> Any {
        guard intent is LatestArticleIntent else {
            fatalError("Unhandled intent type: \(intent)")
        }
        
        
        
        return ArticleIntentHandler()
    }
    
}

Here we check the type of Intent and invoke the AricleIntentHandler.swift class.


import Foundation

class ArticleIntentHandler: NSObject, LatestArticleIntentHandling {
    
    
    func confirm(intent: LatestArticleIntent, completion: @escaping (LatestArticleIntentResponse) -> Void) {
        let apiController = APIController()
        apiController.articleOfTheDay { (articleInfo) in
            if let articleInfo = articleInfo {
                
                if articleInfo.count > 0 {
                    completion(LatestArticleIntentResponse(code: .ready, userActivity: nil))
                } else {
                    completion(LatestArticleIntentResponse(code: .failure, userActivity: nil))
                }
            }
        }
        
    }
    
    func handle(intent: LatestArticleIntent, completion: @escaping (LatestArticleIntentResponse) -> Void) {
        
        let apiController = APIController()
        apiController.articleOfTheDay { (articleInfo) in
            if let title = articleInfo {
                completion(LatestArticleIntentResponse.success(articleTitle: title))
            }
            
        }
    }
}

success is the response code defined in the intent definitions. articleTitle is the parameter
On completion, the response is sent to the Intent Definitions file and the articleTitle if fetched is displayed in Siri.

The Main.storyboard for the JDIntentsUI extension | IntentViewController.swift file is

ios main storyboard intents

The code for the IntentViewController.swift is given below:


import IntentsUI

class IntentViewController: UIViewController, INUIHostedViewControlling {
    
    @IBOutlet weak var myLabel: UILabel!
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
    
    override func viewDidLoad() {
        super.viewDidLoad()        
    }
    
    func configureView(for parameters: Set<INParameter>,
                       of interaction: INInteraction,
                       interactiveBehavior: INUIInteractiveBehavior,
                       context: INUIHostedViewContext,
                       completion: @escaping (Bool, Set, CGSize) -> Void) {
        
        guard interaction.intent is LatestArticleIntent else {
            completion(false, Set(), .zero)
            return
        }
        
        let width = self.extensionContext?.hostedViewMaximumAllowedSize.width ?? 320
        let desiredSize = CGSize(width: width, height: 300)
        
        activityIndicator.startAnimating()
        
        let apiController = APIController()
        
        apiController.articleOfTheDay { (articleInfo) in
            if let articleInfo = articleInfo {
                DispatchQueue.main.async {
                    self.myLabel.text = articleInfo
                    self.activityIndicator.stopAnimating()
                    self.activityIndicator.isHidden = true
                }
                apiController.contentOfArticle{[weak self] (content) in
                    if let content = content {
                        
                        DispatchQueue.main.async {
                            self?.myLabel.text = content
                            self?.activityIndicator.stopAnimating()
                            self?.activityIndicator.isHidden = true
                        }
                    }
                    
                }
                
                
                
            }
        }
        
        completion(true, parameters, desiredSize)
    }
}

Here we are performing back to back API calls. First, we’re retrieving the posts, then in contentOfArticle method from the APIController.swift we fetch the author’s name and update the label in the Intents UI extension with the author’s name.

How to trigger the UI click from Siri to do something?

In the AppDelegate.swift file add the following method:


func application(_ application: UIApplication,
                     continue userActivity: NSUserActivity,
                     restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
        
        guard let url = URL(string: "https://www.journaldev.com/\(APIController.postId)") else {
            return false
        }
        
        
        UIApplication.shared.open(url, options: [:], completionHandler: nil)
        
        
        return true
    }

This launches the url by appending the post id we’d retrieved to the url.

In order to launch a specific ViewController of our application from the Intents UI Extension, we can change the method with the following:


func application(_ application: UIApplication,
                     continue userActivity: NSUserActivity,
                     restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    
        
        guard
            userActivity.interaction?.intent is LatestArticleIntent,
            let window = window,
            let rootViewController = window.rootViewController as? UINavigationController
            else {
                return false
        }

        if rootViewController.viewControllers.count > 1 && rootViewController.viewControllers.last is ViewController {
            return false
        }

        let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
        let vc = storyBoard.instantiateViewController(withIdentifier: "viewController") as! ViewController
        rootViewController.pushViewController(vc, animated: true)
        
        return true
    }

Let’s build and run the application.

The part 1 of the output of the application is(because screen recording stops when siri is up sometimes):

ios siri shortcuts output 1

Part 2:

ios siri shortcuts output 2

You can also add/delete shortcuts from the Settings:

ios siri shortcuts settings

And that brings an end to this tutorial. You can download the project from the link below:

Leave a Reply

Your email address will not be published. Required fields are marked *

close
Generic selectors
Exact matches only
Search in title
Search in content
Search in posts
Search in pages