When you create an Intents Extension, Xcode creates the main class for your extension, named IntentsExtension. This is the class that serves 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. Being on your own means 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 the IntentHandler class conforms 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 a handler method, we could 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 return 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:
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 input for a search query in Core Data, 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. Note 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 successful 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 don't find any results, we tell Siri that we need a value. If we find 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 results found. 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. This could happen if you ask Siri to send a message to a person named Jeff and you have multiple Jeffs in your contact list.