Handling intents in your extension

Handling intents can be divided into three stages. The first stage is the resolving stage. In this stage, your extension will go back and forth with Siri to figure out the correct parameters for the given intent. In this step, Siri could ask your app to verify that a certain username exists, for example. Your extension will then have to figure out if the given input is valid and you'll provide Siri with a response code that tells it whether the parameter is resolved, or maybe requires a little bit more clarification on the user's end. The second stage is expected to confirm that everything is set up correctly and all requirements for executing the action are met. The final step is to actually act on the intent and perform the desired action. Let's go through these stages one by one.

Resolving the user's input

When you create an Intents extension, Xcode creates the main class for your extension named IntentsExtension. This is the class that will serve as an entry point for your Intents extension. It contains a handler(for:) method that returns an instance of Any. The Any type indicates that this method can return virtually anything and the compiler will consider it valid. Whenever you see a method signature like this, you should consider yourself on your own. Meaning that the Swift compiler will not help you to validate that you return the correct instance from this method.

The reason the handler(for:) method returns Any is because this method is supposed to return a handler for every intent that your app supports. If you're handling a send message intent, the handler is expected to conform to the INSendMessageIntentHandling protocol. Xcode's default implementation returns self and makes the IntentHandler class conform to all of the intents the extension handles by default.

This default approach is not inherently bad but if we add an intent to our extension and we forget to implement the handler method, we'd return an invalid object from the handler(for:) method. A cleaner approach is to check the type of intent we're expected to handle and returning an instance of a class that's specialized to handle the intent. This is more maintainable and will allow for a cleaner implementation of both the intent handler itself and the IntentHandler class.

Replacing Xcode's default implementation with the following implementation ensures that we always return the correct object for every intent:

override func handler(for intent: INIntent) -> Any? { 
    if intent is INSendMessageIntent { 
        return SendMessageIntentHandler() 
    } 
 
    return nil 
} 

The SendMessageIntentHandler is a class we'll define and implement to handle the sending of messages. Create a new NSObject subclass named SendMessageIntentHandler and make it conform to INSendMessageIntentHandling. Every intent handler has different required and recommended methods. INSendMessageIntentHandling has just one required method; handle(sendMessage:completion:). Other methods are used to confirm and resolve the intent. We'll look at a single resolve method because they all work similarly; they are just used for different parts of the intent.

Imagine you're building a messaging app that uses groups to send a message to multiple contacts at once. These groups are defined in our app and Siri wants us to resolve a group name. The resolveGroupName(forSendMessage:with:) method is called on the intent handler. This method is now expected to resolve the group name and inform Siri about the result by calling the callback it's been passed. Let's see how in the following code:

let supportedGroups = ["neighbors", "coworkers", "developers"] 
func resolveGroupName(forSendMessage intent: INSendMessageIntent, with completion: @escaping (INStringResolutionResult) -> Void) { 
     
    guard let givenGroupName = intent.groupName else { 
        completion(.needsValue()) 
        return 
    } 
     
    let matchingGroups = supportedGroups.filter{ group in 
        return group.contains(givenGroupName) 
    } 
     
    switch matchingGroups.count { 
    case 0: 
        completion(.needsValue()) 
    case 1: 
        completion(.success(with: matchingGroups.first!)) 
    default: 
        completion(.disambiguation(with: matchingGroups)) 
    } 
} 

In order to simplify the example a bit, the supported groups are defined as an array. In reality, you would use the given group name as the input for a search query in CoreData, your server, or any other place where you might have stored the information about contact groups.

The method itself first makes sure that a group name is present on the intent. If it's not, we'll tell Siri that a group name is required for our app. Not that this might not be desirable for all messaging apps. Actually, most messaging apps will allow users to omit the group name altogether. If this is the case, you'd call the completion handler with a success result.

If a group name is given, we look for matches in the supportedGroups array. Again, most apps would query an actual database at this point. If we didn't find any results, we'll tell Siri that we need a value. If we found a single result, we're done. We can tell Siri that we successfully managed to match the intent's group with a group in our database. If we have more than one result, we tell Siri that we need to disambiguate the found results. Siri will then take care of asking the user to specify which one of the provided inputs should be used to send the message to.

Confirming the intent status

After you've made sure that everything you need to eventually handle the intent is in place, you must confirm this to Siri. Every intent handler has a confirm method. The signature might vary, but there is always some form of confirmation in place. Refer to the documentation for the intent you're handling to make sure which method you're expected to implement. When you're sending messages, the confirmation method is confirm(sendMessage:completion:).

We can make this confirmation step as complex as we desire. For example, we could check whether a message is too long, contains forbidden content, or virtually anything else. Most commonly, you'll want to make sure that the user is authenticated and allowed to send a message to the recipient.

Again, it's completely up to your best judgement to determine which preconditions apply to your extension. The important takeaway for the confirm method is that you're expected to make sure that everything is in place to smoothly perform the action later.

Let's look at an example of a confirmation implementation to see some of the possible outcomes of the confirmation step in the following code:

func confirm(sendMessage intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Void) { 
    guard let user = User.current(), user.isLoggedIn else { 
        completion(INSendMessageIntentResponse(code: .failureRequiringAppLaunch, userActivity: nil)) 
        return 
    } 
     
    guard MessagingApi.isAvailable else { 
        completion(INSendMessageIntentResponse(code: .failureMessageServiceNotAvailable, userActivity: nil)) 
        return 
    } 
     
    completion(INSendMessageIntentResponse(code: .ready, userActivity: nil)) 
} 

The preceding implementation checks if a current user is available, and whether they are logged in or not. Also, the availability of the API that will eventually handle the message sending is checked. Note that these two classes don't exist in the example project and they should be defined by you if you decide to go with this confirmation approach. These classes simply serve as placeholder examples to demonstrate how confirmation of an intent works.

If we don't have a logged in user, we launch the app. If we don't have a messaging API available, we return an error message that reflects the fact that the service is not available. Every intent has their own set of response codes. Refer to the documentation for the intent you're handling to find the relevant response code and decide which ones are appropriate to handle in your extension.

If your user must be taken to your app in order to log in, for example, Siri will automatically create a user activity that's passed to AppDelegate in your application. You must implement application(_:continue:restorationHandler:) to catch and continue the user activity. Just like when we used user activities with Spotlight, it's important that you take the shortest path possible to resume and handle the user activity.

A user activity that's created by Siri has its interaction property set. This property will contain an INInteraction object that reflects the action the user attempts to complete using Siri. A good implementation will fulfill this interaction as soon as possible inside of the app. It's also possible to create your own user activity if you want to add custom information that Siri doesn't pass on. If you want to do this, you should pass user activity to the INSendMessageIntentResponse initializer.

After confirming that everything is in place, it's time to actually perform the action for the user.

Performing the desired action

Once Siri knows exactly what the user wants to do and which parameters to use, and once your app has confirmed that everything is in place to handle the user's request, the time has finally come to do so.

Siri will call the handle method on your intent handler. Just like the confirm method, every intent will have their own version of this but they all follow a similar pattern. For sending messages, the method signature is handle(sendMessage:completion:). The parameters for this message are identical to the ones in the confirmation step. The major difference is that you're now expected to handle the intent.

Once you're done handling the intent, you need to call the completion handler with an INSendMessageIntentResponse. If everything goes well, you're expected to use a success response code. If you're unable to process the intent quickly, you're expected to call the completion handler with an inProgress status code. This informs Siri that you're handling the intent but it's taking a while. An example of a handle method is shown in the following code:

func handle(sendMessage intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Void) { 
    guard let groupName = intent.groupName, 
        let message = intent.content else { 
             
            completion(INSendMessageIntentResponse(code: .failure, 
              userActivity: nil)) 
    } 
     
    MessagingApi.sendMessage(message, toGroup: groupName) { success in 
        if success { 
            completion(INSendMessageIntentResponse(code: .success, 
              userActivity: nil) 
        } else { 
            completion(INSendMessageIntentResponse(code: .failure, 
              userActivity: nil) 
        } 
    } 
} 

Just like before, we're using non-existing classes so copy pasting this code won't work in the example app. The purpose of this snippet is to show you what an implementation of sending a message could look like. First, we confirm that we can extract all of the required information to send a message with. If this fails, we tell Siri that we couldn't send the message. Then a MessagingApi class method is used to send the message to the selected group. Finally, we inform Siri about how we handled the intent based on the response from the API.

The final aspect of the Siri experience we need to take into account is the user interface. Siri will provide us with a default interface for every intent but it's often desirable to customize this interface to match your app's look and feel. Let's see how we can achieve this.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.162.37