Resolving the user's input

When you create an intents extension, Xcode creates a single main class for your extension, named IntentsExtension. This is the class that serves as an entry point for your 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've returned the correct type of object from this method.

In addition to the handler(for:) method, Xcode has generated a lot of sample code to show how you could implement an intent handler. After quickly studying this code, you can go ahead and remove it, since you'll write your own implementation for this functionality soon.

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, according to its plist file.

This default approach is not inherently wrong, but if you add an intent to your extension and forget to implement a handler method, you might return an invalid object from the handler(for:) method. A cleaner approach is to check the type of intent you'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 ensures that you always return the correct object for every intent that the Hairdressers app supports. Go ahead and replace the handler(for:) method with the following implementation:

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

  return nil
}

The SendMessageIntentHandler is a class you will define and implement to handle the sending of messages. Create a new NSObject subclass, named SendMessageIntentHandler, and make it conform to INSendMessageIntentHandling. When you create this file, make sure it's added to the MessageHairdresserIntent target, and not the Hairdressers app target.

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. All of the resolve methods work in similar ways, but are used for different parameters in an 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 your app and Siri wants to resolve a group name. If this occurs, Siri calls the resolveGroupName(forSendMessage:with:) method 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 this works:

let supportedGroups = ["neighbors", "coworkers", "developers"]

func resolveGroupName(forSendMessage intent: INSendMessageIntent, with completion: @escaping (INStringResolutionResult) -> Void) {

  guard let givenGroupName = intent.speakableGroupName 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))
  }
}

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, a callback is used to inform Siri that a group name is required for this 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, it is used to filter the supportedGroups array. Again, most apps would query an actual database at this point. If no results are found, Siri is asked for a value. If a single result is found, the work is done. The code successfully managed to match the intent's group with a group in the app's database and Siri is informed accordingly. If more than one result was found, Siri is asked to disambiguate the results that were 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, but you have multiple Jeffs in your contact list.

In the case of the Hairdressers app, messages are sent to individual hairdressers that are stored in the Hairdressers.plist file. The HairdressersDataSource helper object can read the data from this plist and provides a simple array of hairdresser names. Since this data is currently only part of the Hairdressers app target, you will need to add it to the MessageHairdresserIntent target as well.

To do this, select both of the files in the Project Navigator, and use the File Inspector on the right side of the Xcode window to add these files to the MessageHairdresserIntent target, as shown in the following screenshot:

Next, add the following extension to SendMessageIntentHandler.swift to implement resolveRecipients(for:with:):

extension SendMessageIntentHandler: INSendMessageIntentHandling {
  func resolveRecipients(for intent: INSendMessageIntent, with completion: @escaping ([INSendMessageRecipientResolutionResult]) -> Void) {

    guard let recipients = intent.recipients else {
      completion([.needsValue()])
      return
    }

    let results: [INSendMessageRecipientResolutionResult] = recipients.map { person in
      let matches = HairdressersDataSource.hairdressers.filter { hairdresser in
        return hairdresser == person.displayName
      }

      switch matches.count {
      case 0: return INSendMessageRecipientResolutionResult.needsValue()
      case 1: return INSendMessageRecipientResolutionResult.success(with: person)
      default: return INSendMessageRecipientResolutionResult.disambiguation(with: [person])
      }
    }

    completion(results)
  }
}

The preceding code should look very familiar, because it's very similar to the code for resolving a group name. The main difference is that a user can choose multiple recipients for the message, so a resolution result is created for each of the recipients that Siri passes to the resolveRecipients(for:with:) method. The next stage in handling the intent is to confirm the intent status.