Customizing the SiriKit Intent User Interface

From Techotopia
Revision as of 19:43, 11 April 2018 by Neil (Talk | contribs) (Created page with "{{#pagetitle: Customizing the SiriKit Intent User Interface}} <seo title="Customizing the SiriKit Intent User Interface" titlemode="replace" keywords="ios 11, swift 4, sirikit...")

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search


Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book


Each SiriKit domain will default to a standard user interface layout to present information to the user during the Siri session. In the previous chapter, for example, the standard user interface was used by SiriKit to display to the user the message recipients and content to the user before sending the message. The default appearance can, however, be customized by making use of an Intent UI app extension. This UI Extension provides a way to control the appearance of information when it is displayed within the Siri interface. It also allows an extension to present additional information that would not normally be displayed by Siri or to present information using a visual style that reflects the design theme of the main app.


Contents


Modifying the UI Extension

SiriKit provides two mechanisms for performing this customization each of which involves implementing a method in the intent UI view controller class file. A simpler and less flexible option involves the use of the configure method. For greater control, the previously mentioned configureView method is available.

Using the configure Method

When the Intents Extension target was added to the SiriKitDemo project in the previous chapter, the option to also include a UI Extension was selected. The files for this extension can be found within the Project navigator panel under the SiriDemoIntentUI folder.

Included within the SiriDemoIntentUI extension is a storyboard file named MainInterface.storyboard. When loaded into Interface Builder it will become clear that this currently contains a single ViewController scene and a single UIView object. When the configure method is used to customize the user interface, this scene is used to display additional content which will appear directly above the standard SiriKit provided UI content. This layout is sometimes referred to as the Siri Snippet. For comparison purposes, consider the default UI when sending a message using SiriKit:


Ios 11 sirikit demo default ui.png

Figure 97-1


Although not visible by default, at the top of the message panel is the area represented by the UI Extension (had we not requested the UI Extension when the Intents Extension was added, this area would not be present). Specifically, this displays the scene defined in the MainInterface.storyboard file of the SiriDemoIntentUI extension folder. The lower section of the panel is the default user interface provided by Siri for this particular SiriKit domain.

To provide a custom user interface using the UI Extension, the user interface needs to be implemented in the MainInterface.storyboard file and the configure method added to the IntentViewController.swift file. The IntentViewController class in this file is a subclass of UIViewController and configured such that it implements the INUIHostedViewControlling protocol.

The UI Extension is only used when information is being presented to the user in relation to an intent type that has been declared as supported in the UI Extension’s Info.plist file. When the extension is used, the configure method of the IntentViewController is called and passed an INInteraction object containing both the NSUserActivity and intent objects associated with the current Siri session. This allows context information about the session to be extracted and displayed to the user via the custom user interface defined in the MainInterface.storyboard file.

To add content above the “To:” line, therefore, we just need to implement the configure method and add some views to the UIView instance in the storyboard file. These views can be added either via Interface Builder or programmatically with the configure method.


Designing the Siri Snippet Scene

For the purposes of this example, the storyboard scene for the UI Extension will be configured to display the content of the message on a white label with a black background. Keep in mind, however, that views of any type and combination can be used in the layout, including images and maps. Select the MainInterface.storyboard file and drag and drop a Label object so that it is positioned in the center of the scene. Using the Auto Layout Resolve Auto Layout Issues menu, select the option to Reset to Suggested Constraints.

Select the Label object, display the Attributes Inspector panel and change the color property to white. Next, select the background view and change the background color to black. On completion of these steps, the layout should match Figure 97-2:


Ios 11 sirikit label ui.png

Figure 97-2


Display the Assistant Editor and establish an outlet connection from the Label object named contentLabel within the IntentViewController.swift class file.

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

Adding the configure Method

The configure method now needs to be written to detect if the current intent is a send message intent, extract the message content from the intent object and display it on the Label object within the storyboard scene. Edit the IntentViewController.swift file and add the configure method as follows:

func configure(with interaction: INInteraction, context:
    INUIHostedViewContext, completion: @escaping ((CGSize) -> Void)) {
    
    if interaction.intent is INSendMessageIntent {
        let intent = interaction.intent as! INSendMessageIntent
        self.contentLabel.text = intent.content
    }
    
    completion(self.desiredSize)
}

The added code simply checks that the intent is of type INSendMessageIntent, extracts the intent from the NSInteraction object and then assigns the message content contained within the intent to the text property of the Label object.

If a class contains both the configure and configureView methods, Siri will use the configureView method by default. To prevent this, comment out the configureView method as follows before testing the extension:

/*
func configureView(for parameters: Set<INParameter>, of interaction: 
   INInteraction, interactiveBehavior: INUIInteractiveBehavior, context: 
     INUIHostedViewContext, completion: @escaping (Bool, Set<INParameter>, 
       CGSize) -> Void) {

    if parameters.isEmpty {
        completion(false, [], CGSize.zero)
    }
}
*/

Next, the desiredSize declaration needs to be changed so that the user interface does not use the maximum allowed size (which is much larger than is actually needed for this example):

var desiredSize: CGSize {
    //return self.extensionContext!.hostedViewMaximumAllowedSize
    return CGSize.init(width: 10, height: 100)
}

Compile and run the extension and use Siri to prepare a message so that it is ready to be sent. This time, the UI Extension section should appear as shown in Figure 97-3:


Ios 11 sirikit message with snippet.png

Figure 97-3

Clearly the extension user interface is working. There is, however, an issue in that the message content is now displayed twice, once by the UI Extension and once by Siri. A preferable outcome would be for the message only to appear in the UI Extension. This can be achieved using a feature referred to as Siri override control.

Overriding Siri Content

Siri override control defines a system whereby a UI Extension can let Siri know that it will take responsibility for displaying certain types of information, thereby avoiding duplicated content within the Siri user interface. Currently SiriKit only allows maps, message content and payment transactions to be replaced by the UI Extension.

Override control requires that the intent view controller implement the INUIHostingViewSiriProviding protocol together with the methods for the content to be overridden. Available methods are as follows:

  • displaysMap
  • displaysMessage
  • displaysPaymentTransaction

The methods return a Boolean value indicating whether or not the Extension UI will be displaying the content.

In this case, the displaysMessage method needs to be added and implemented such that it returns a true value. Edit the IntentViewController.swift file and implement these changes as follows:

import IntentsUI

class IntentViewController: UIViewController, INUIHostedViewControlling, INUIHostedViewSiriProviding {
.
.
    var displaysMessage: Bool {
        return true
    }
.
.
}

Run the intent extension one last time, prepare a message using Siri and verify that the message content is no longer duplicated by Siri:


Ios 11 sirikit message with snippet no message.png

Figure 97-4

Using the configureView Method

Unlike the configure method, the configureView method allows each section of the default user interface to be replaced with custom content and view layout.

SiriKit considers the default layout to be a vertical stack in which each row is represented by a parameter. In Figure 97 1 above, for example, the recipients parameter is displayed in the uppermost row of the stack while the message content appears beneath it in the second row.

For each layer of the stack (starting at the top and finishing at the bottom of the layout) the configureView method is called, passed information about the corresponding parameters and provided the opportunity to provide a custom layout to be displayed within the corresponding stack row of the Siri user interface. The method is also passed a completion handler to be called with the appropriate configuration information to be passed back to Siri.

The parameters passed to the method take the form of INParameter instances. It is the responsibility of the configureView method to find out if a parameter is one for which it wants to provide a custom layout. It does this by creating local NSParameter instances of the type it is interested in and comparing these to the parameters passed to the method. Parameter instances are created by combining the intent class type with a specific key path representing the parameter (each type of intent has its own set of path keys which can be found in the documentation for that class). If the method needs to confirm that the passed parameter relates to the content of a send message intent, for example, the code would read as follows:

func configureView(for parameters: Set<INParameter>, of interaction: 
   INInteraction, interactiveBehavior: INUIInteractiveBehavior, context: 
    INUIHostedViewContext, completion: @escaping (Bool, Set<INParameter>, 
      CGSize) -> Void) {

    let content = INParameter(for: INSendMessageIntent.self, 
               keyPath: #keyPath(INSendMessageIntent.content))

    if parameters == [content] {
       // Configure ViewController before calling completion handler
   }
.
.
}

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

When creating a custom layout, it is likely that the method will need to access the data contained within the parameter. In the above code, for example, it might be useful to extract the message content from the parameter and incorporate it into the custom layout. This is achieved by calling the parameterValue method of the INInteraction object which is also passed to the configureView method. Each parameter type has associated with it a set of properties. In this case, the property for the message content is named, appropriately enough, content and can be accessed as follows:

.
.
let content = INParameter(for: INSendMessageIntent.self, 
               keyPath: #keyPath(INSendMessageIntent.content))

if parameters == [content] {
   let contentString = interaction.parameterValue(for: content)
}
.
.

When the configureView method is ready to provide Siri with a custom layout, it calls the provided completion handler, passing through a Boolean true value, the original parameters and a CGSize object defining the size of the layout as it is to appear in the corresponding row of the Siri user interface stack, for example:

completion(true, parameters, size)

If the default Siri content is to be displayed for the specified parameters instead of a custom user interface, the completion handler is called with a false value and a zero CGSize object:

completion(false, parameters, CGSize.zero)

In addition to calling the configureView method for each parameter, Siri will first make a call to the method to request a configuration for no parameters. By default, the method should check for this condition and call the completion handler as follows:

if parameters.isEmpty {
    completion(false, [], CGSize.zero)
}

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

The foundation for the custom user interface for each parameter is the View contained within the intent UI MainInterface.storyboard file. Once the configureView method has identified the parameters it can dynamically add views to the layout, or make changes to existing views contained within the scene.

Implementing a configureView Custom UI

The previous section covered a considerable amount of information, much of which will become clearer by working through an example. Begin, therefore, by editing the IntentViewController.swift file, deleting the configure method added earlier in the chapter and removing the comment markers around the configureView method.

Next, open the MainInterface.storyboard file belonging to the SiriDemoIntentUI extension and resize the Label view so that it stretches to the left and right margin markers. With the Label still selected, display the Attributes Inspector and change the Alignment setting so that the text is left aligned, then use the Resolve Auto Layout Issues menu to set suggested constraints on all views. On completion of these steps, the layout should match that of Figure 97-5:


Ios 11 sirikit label left align.png

Figure 97-5


Before proceeding to the next step, establish an outlet connection from the View component to a variable in the IntentViewController.swift file named contentView.

Next, edit the configureView method to check for the recipients parameter as follows:

func configureView(for parameters: Set<INParameter>, of interaction: 
    INInteraction, interactiveBehavior: INUIInteractiveBehavior, context: 
    INUIHostedViewContext, completion: @escaping (Bool, Set<INParameter>, 
     CGSize) -> Void) {

    var size = CGSize.zero
    
    let recipients = INParameter(for: INSendMessageIntent.self, 
                       keyPath: #keyPath(INSendMessageIntent.recipients))
        
    if parameters == [recipients] {

        let recipientsValue = interaction.parameterValue(
			for: recipients) as! Array<INPerson>

    } else if parameters.isEmpty {

        completion(false, [], CGSize.zero)

    }    

    completion(true, parameters, size)
}

The recipients parameter takes the form of an array of INPerson objects, from which can be extracted the recipients’ display names. Code now needs to be added to iterate through each recipient in the array, adding each name to a string to be displayed on the contentLabel view. Code will also be added to use a different font on the label and to change the background color of the view:

.
.
if parameters == [recipients] {
    let recipientsValue = interaction.parameterValue(
                    for: recipients) as! Array<INPerson>
    
    var recipientStr = "To:"
    var first = true
    
    for name in recipientsValue {
        let separator = first ? " " : ", "
        first = false
        recipientStr += separator + name.displayName
    }
    
    self.contentLabel.font = UIFont(name: "Arial-BoldItalicMT", size: 20.0)
    self.contentLabel.text = recipientStr
    self.contentView.backgroundColor = UIColor.blue
    size = CGSize(width: 100, height: 30)
} else if parameters.isEmpty {
    completion(false, [], CGSize.zero)
}

completion(true, parameters, size)
.
. 

Compile and run the intent extension and verify that the recipient row now appears with a blue background, a 30 point height and uses a larger italic font. The content row, on the other hand, should continue to use the Siri default UI:


Ios 11 sirikit cutom recipient.png

Figure 97-6


Finally, add code to the method to provide a custom UI for the content parameter:

.
.
let recipients = INParameter(for: INSendMessageIntent.self, keyPath: 
                          #keyPath(INSendMessageIntent.recipients))

let content = INParameter(for: INSendMessageIntent.self, keyPath: 
                          #keyPath(INSendMessageIntent.content))

if parameters == [recipients] {
    let recipientsValue = interaction.parameterValue(
                    for: recipients) as! Array<INPerson>
    
    var recipientStr = "To:"
    var first = true
    
    for name in recipientsValue {
        let separator = first ? " " : ", "
        first = false
        recipientStr += separator + name.displayName
    }
    
    self.contentLabel.font = UIFont(name: "Arial-BoldItalicMT", size: 20.0)
    self.contentLabel.text = recipientStr
    self.contentView.backgroundColor = UIColor.blue
    size = CGSize(width: 100, height: 30)
} else if parameters == [content] {

    let contentValue = interaction.parameterValue(for: content)

    self.contentLabel.text = contentValue as? String
    self.contentView.backgroundColor = UIColor.brown
    size = CGSize(width: 100, height: 70)
} else if parameters.isEmpty {
    completion(false, [], CGSize.zero)
}

completion(true, parameters, size)
}

Run the extension one last time and verify that the content appears on the contentLabel view and that the view has a brown background and a 70px height:


Ios 11 sirikit cutom content.png

Figure 97-7

Summary

While the default user interface provided by SiriKit for the various domains will be adequate for some apps, most intent extensions will need to be customized to present information in a way that matches the style and theme of the associated app, or to provide additional information not supported by the default layout. The default UI can be replaced by adding an Intent UI extension to the app project. The UI extension provides two options for configuring the user interface presented by Siri. The simpler of the two involves the use of the configure method to present a custom view above the default Siri user interface layout. A more flexible approach involves the implementation of the configureView method. SiriKit associates each line of information displayed in the default layout with a parameter. When implemented, the configureView method will be called for each of these parameters and provided with the option to return a custom View containing the layout and information to be used in place of the default user interface element


Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book