As intenções do SiriKit se encaixam no seu aplicativo? Se sim, veja como usá-los

Publicados: 2022-03-10
Resumo rápido ↬ Desde o ano passado, é possível adicionar o suporte Siri a um aplicativo se ele se encaixar em um dos casos de uso predefinidos da Apple. Descubra se o SiriKit funcionará para você e como usá-lo.

Desde o iOS 5, a Siri ajuda os usuários do iPhone a enviar mensagens, definir lembretes e procurar restaurantes com os aplicativos da Apple. A partir do iOS 10, também podemos usar a Siri em alguns de nossos próprios aplicativos.

Para usar essa funcionalidade, seu aplicativo deve se encaixar nos “domínios e intenções” predefinidos da Siri da Apple. Neste artigo, aprenderemos sobre o que são e veremos se nossos aplicativos podem usá-los. Pegaremos um aplicativo simples que é um gerenciador de lista de tarefas e aprenderemos como adicionar suporte à Siri. Também analisaremos as diretrizes do site do desenvolvedor da Apple sobre configuração e código Swift para um novo tipo de extensão que foi introduzido com o SiriKit: a extensão Intents .

Quando você chegar à parte de codificação deste artigo, precisará do Xcode (pelo menos a versão 9.x), e seria bom se você estivesse familiarizado com o desenvolvimento do iOS em Swift, porque adicionaremos a Siri a um pequeno aplicativo. Passaremos pelas etapas de configuração de uma extensão no site do desenvolvedor da Apple e de adicionar o código de extensão Siri ao aplicativo.

“Ei Siri, por que preciso de você?”

Às vezes, uso meu telefone enquanto estou no sofá, com as duas mãos livres, e posso dar toda a atenção à tela. Talvez eu mande uma mensagem para minha irmã para planejar o aniversário da nossa mãe ou responda a uma pergunta no Trello. Eu posso ver o aplicativo. Eu posso tocar na tela. Eu posso digitar.

Mas posso estar andando pela minha cidade, ouvindo um podcast, quando um texto chega no meu relógio. Meu telefone está no bolso e não consigo atender facilmente enquanto ando.

Mais depois do salto! Continue lendo abaixo ↓

Com a Siri, posso manter pressionado o botão de controle do meu fone de ouvido e dizer: “Envie uma mensagem para minha irmã dizendo que estarei lá às duas horas”. A Siri é ótima quando você está em movimento e não consegue dar total atenção ao seu telefone ou quando a interação é pequena, mas requer vários toques e muita digitação.

Isso é bom se eu quiser usar aplicativos da Apple para essas interações. Mas algumas categorias de aplicativos, como mensagens, têm alternativas muito populares. Outras atividades, como reservar uma carona ou reservar uma mesa em um restaurante, nem são possíveis com os aplicativos integrados da Apple, mas são perfeitas para a Siri.

A abordagem da Apple aos assistentes de voz

Para habilitar o Siri em aplicativos de terceiros, a Apple teve que decidir sobre um mecanismo para tirar o som da voz do usuário e de alguma forma levá-lo ao aplicativo de forma que pudesse atender à solicitação. Para tornar isso possível, a Apple exige que o usuário mencione o nome do aplicativo na solicitação, mas eles tinham várias opções sobre o que fazer com o restante da solicitação.

  • Poderia ter enviado um arquivo de som para o aplicativo.
    O benefício dessa abordagem é que o aplicativo pode tentar lidar literalmente com qualquer solicitação que o usuário possa ter para ele. A Amazon ou o Google podem ter gostado dessa abordagem porque já possuem serviços sofisticados de reconhecimento de voz. Mas a maioria dos aplicativos não seria capaz de lidar com isso com muita facilidade.
  • Poderia ter transformado o discurso em texto e enviado isso.
    Como muitos aplicativos não têm implementações sofisticadas de linguagem natural, o usuário normalmente teria que se ater a frases muito específicas, e o suporte em outros idiomas ficaria a cargo do desenvolvedor do aplicativo implementar.
  • Ele poderia ter solicitado que você fornecesse uma lista de frases que você entende.
    Esse mecanismo está mais próximo do que a Amazon faz com o Alexa (em sua estrutura de “habilidades”) e permite muito mais usos do Alexa do que o SiriKit pode lidar atualmente. Em uma habilidade do Alexa, você fornece frases com variáveis ​​de espaço reservado que o Alexa preencherá para você. Por exemplo, “Alexa, lembre-me em $TIME$ to $REMINDER$ ” — Alexa executará essa frase com base no que o usuário disse e informará os valores de TIME e REMINDER . Assim como no mecanismo anterior, o desenvolvedor precisa fazer toda a tradução e não há muita flexibilidade se o usuário disser algo ligeiramente diferente.
  • Ele pode definir uma lista de solicitações com parâmetros e enviar ao aplicativo uma solicitação estruturada.
    Isso é realmente o que a Apple faz, e o benefício é que ela pode suportar uma variedade de idiomas, e faz todo o trabalho para tentar entender todas as maneiras pelas quais um usuário pode formular uma solicitação. A grande desvantagem é que você só pode implementar manipuladores para solicitações que a Apple define. Isso é ótimo se você tiver, por exemplo, um aplicativo de mensagens, mas se tiver um serviço de streaming de música ou um player de podcast, não terá como usar o SiriKit agora.

Da mesma forma, existem três maneiras de os aplicativos responderem ao usuário: com som, com texto que é convertido ou expressando o tipo de coisa que você quer dizer e deixando o sistema descobrir a maneira exata de expressá-lo. A última solução (que é o que a Apple faz) coloca o ônus da tradução na Apple, mas oferece maneiras limitadas de usar suas próprias palavras para descrever as coisas.

Os tipos de solicitações que você pode manipular são definidos nos domínios e intenções do SiriKit. Uma intenção é um tipo de solicitação que um usuário pode fazer, como enviar uma mensagem de texto para um contato ou encontrar uma foto. Cada intent tem uma lista de parâmetros — por exemplo, mensagens de texto requerem um contato e uma mensagem.

Um domínio é apenas um grupo de intenções relacionadas. Ler um texto e enviar um texto estão ambos no domínio de mensagens. Reservar uma viagem e obter um local está no domínio de reserva de viagem. Existem domínios para fazer chamadas VoIP, iniciar treinos, pesquisar fotos e mais algumas coisas. A documentação do SiriKit contém uma lista completa de domínios e suas intenções.

Uma crítica comum à Siri é que ela parece incapaz de lidar com solicitações tão bem quanto Google e Alexa, e que o ecossistema de voz de terceiros habilitado pelos concorrentes da Apple é mais rico.

Concordo com essas críticas. Se seu aplicativo não se encaixar nas intenções atuais, você não poderá usar o SiriKit e não poderá fazer nada. Mesmo que seu aplicativo se encaixe, você não pode controlar todas as palavras que a Siri diz ou entende; então, se você tem uma maneira específica de falar sobre as coisas em seu aplicativo, nem sempre pode ensinar isso à Siri.

A esperança dos desenvolvedores do iOS é que a Apple expanda bastante sua lista de intenções e que seu processamento de linguagem natural se torne muito melhor. Se isso acontecer, teremos um assistente de voz que funciona sem que os desenvolvedores precisem fazer tradução ou entender todas as maneiras de dizer a mesma coisa. E implementar o suporte para solicitações estruturadas é bastante simples de fazer — muito mais fácil do que construir um analisador de linguagem natural.

Outro grande benefício do framework de intents é que ele não se limita a Siri e solicitações de voz. Mesmo agora, o aplicativo Maps pode gerar uma solicitação baseada em intents do seu aplicativo (por exemplo, uma reserva em um restaurante). Ele faz isso programaticamente (não por voz ou linguagem natural). Se a Apple permitisse que os aplicativos descobrissem as intenções expostas uns dos outros, teríamos uma maneira muito melhor de os aplicativos trabalharem juntos (em oposição aos URLs no estilo x-callback).

Por fim, como um intent é uma solicitação estruturada com parâmetros, há uma maneira simples de um aplicativo expressar que os parâmetros estão ausentes ou que precisa de ajuda para distinguir entre algumas opções. A Siri pode fazer perguntas de acompanhamento para resolver os parâmetros sem que o aplicativo precise conduzir a conversa.

O domínio de reserva de viagem

Para entender os domínios e as intenções, vejamos o domínio de reserva de viagem. Este é o domínio que você usaria para pedir à Siri um carro Lyft.

A Apple define como pedir uma carona e como obter informações sobre isso, mas na verdade não há um aplicativo integrado da Apple que possa realmente lidar com essa solicitação. Este é um dos poucos domínios em que é necessário um aplicativo habilitado para SiriKit.

Você pode invocar uma das intenções por voz ou diretamente do Maps. Algumas das intenções para este domínio são:

  • Solicite uma carona
    Use este para reservar um passeio. Você precisará fornecer um local de embarque e desembarque, e o aplicativo também pode precisar saber o tamanho da sua festa e que tipo de passeio você deseja. Um exemplo de frase pode ser: "Reserve uma carona para mim com <appname>".
  • Obter o status da viagem
    Use essa intenção para descobrir se sua solicitação foi recebida e obter informações sobre o veículo e o motorista, incluindo sua localização. O aplicativo Mapas usa essa intenção para mostrar uma imagem atualizada do carro conforme ele se aproxima de você.
  • Cancelar uma viagem
    Use isso para cancelar uma viagem que você reservou.

Para qualquer uma dessas intenções, a Siri pode precisar de mais informações. Como você verá quando implementarmos um manipulador de intents, sua extensão de intents pode informar à Siri que um parâmetro obrigatório está ausente e a Siri solicitará isso ao usuário.

O fato de os intents poderem ser invocados programaticamente pelo Maps mostra como os intents podem habilitar a comunicação entre aplicativos no futuro.

Nota : Você pode obter uma lista completa de domínios e suas intenções no site do desenvolvedor da Apple. Há também um aplicativo Apple de amostra com muitos domínios e intenções implementados, incluindo reserva de viagens.

Adicionando suporte de domínio de listas e notas ao seu aplicativo

OK, agora que entendemos o básico do SiriKit, vamos ver como você adicionaria suporte ao Siri em um aplicativo que envolve muita configuração e uma classe para cada intent que você deseja manipular.

O restante deste artigo consiste nas etapas detalhadas para adicionar suporte à Siri a um aplicativo. Há cinco coisas de alto nível que você precisa fazer:

  1. Prepare-se para adicionar uma nova extensão ao aplicativo criando perfis de provisionamento com novos direitos para ele no site do desenvolvedor da Apple.
  2. Configure seu aplicativo (por meio de seu plist ) para usar os direitos.
  3. Use o modelo do Xcode para começar com algum código de exemplo.
  4. Adicione o código para dar suporte à sua intenção Siri.
  5. Configure o vocabulário da Siri via plist s.

Não se preocupe: vamos passar por cada um deles, explicando extensões e direitos ao longo do caminho.

Para focar apenas nas partes da Siri, preparei um gerenciador de lista de tarefas simples, o List-o-Mat.

Um GIF animado mostrando uma demonstração do List-o-Mat
Fazendo listas no List-o-Mat (visualização grande)

Você pode encontrar a fonte completa do exemplo, List-o-Mat, no GitHub.

Para criá-lo, tudo o que fiz foi começar com o modelo de aplicativo Xcode Master-Detail e transformar as duas telas em um UITableView . Eu adicionei uma maneira de adicionar e excluir listas e itens e uma maneira de marcar os itens como concluídos. Toda a navegação é gerada pelo template.

Para armazenar os dados, usei o protocolo Codable , (introduzido na WWDC 2017), que transforma structs em JSON e salva em um arquivo texto na pasta de documents .

Eu deliberadamente mantive o código muito simples. Se você tiver alguma experiência com o Swift e criar controladores de exibição, não deverá ter problemas com isso.

Agora podemos seguir as etapas de adição de suporte ao SiriKit. As etapas de alto nível seriam as mesmas para qualquer aplicativo e qualquer domínio e intents que você planeja implementar. Nós estaremos lidando principalmente com o site de desenvolvedores da Apple, editando plist e escrevendo um pouco de Swift.

Para List-o-Mat, vamos nos concentrar no domínio de listas e notas, que é amplamente aplicável a coisas como aplicativos de anotações e listas de tarefas.

No domínio de listas e notas, temos as seguintes intenções que fariam sentido para nosso aplicativo.

  • Obtenha uma lista de tarefas.
  • Adicione uma nova tarefa a uma lista.

Como as interações com a Siri realmente acontecem fora do seu aplicativo (talvez mesmo quando o aplicativo não está em execução), o iOS usa uma extensão para implementar isso.

A extensão de intenções

Se você não trabalhou com extensões, precisará saber três coisas principais:

  1. Uma extensão é um processo separado. Ele é fornecido dentro do pacote do seu aplicativo, mas é executado completamente por conta própria, com seu próprio sandbox.
  2. Seu aplicativo e extensão podem se comunicar por estarem no mesmo grupo de aplicativos. A maneira mais fácil é através das pastas de sandbox compartilhadas do grupo (assim, eles podem ler e gravar nos mesmos arquivos se você os colocar lá).
  3. As extensões exigem seus próprios IDs de aplicativo, perfis e direitos.

Para adicionar uma extensão ao seu aplicativo, comece fazendo login na sua conta de desenvolvedor e vá para a seção "Certificados, identificadores e perfis".

Atualizando os dados da conta do aplicativo Apple Developer

Em nossa conta de desenvolvedor da Apple, a primeira coisa que precisamos fazer é criar um grupo de aplicativos. Vá para a seção "Grupos de aplicativos" em "Identificadores" e adicione um.

Uma captura de tela da caixa de diálogo do site do desenvolvedor da Apple para registrar um grupo de aplicativos
Registrando um grupo de aplicativos (visualização grande)

Ele deve começar com group , seguido por seu identificador habitual baseado em domínio reverso. Por ter um prefixo, você pode usar o identificador do seu aplicativo para o resto.

Em seguida, precisamos atualizar o ID do nosso aplicativo para usar este grupo e habilitar a Siri:

  1. Vá para a seção “App IDs” e clique no ID do seu app;
  2. Clique no botão “Editar”;
  3. Habilite grupos de aplicativos (se não habilitado para outra extensão).
    Uma captura de tela do site do desenvolvedor da Apple ativando grupos de aplicativos para um ID de aplicativo
    Ativar grupos de aplicativos (visualização grande)
  4. Em seguida, configure o grupo de aplicativos clicando no botão “Editar”. Escolha o grupo de aplicativos de antes.
    Uma captura de tela da caixa de diálogo do site do desenvolvedor da Apple para definir o nome do grupo de aplicativos
    Defina o nome do grupo de aplicativos (visualização grande)
  5. Ative o SiriKit.
    Uma captura de tela do SiriKit sendo ativado
    Ativar SiriKit (visualização grande)
  6. Clique em “Concluído” para salvá-lo.

Agora, precisamos criar um novo ID de aplicativo para nossa extensão:

  1. Na mesma seção "IDs do aplicativo", adicione um novo ID do aplicativo. Este será o identificador do seu aplicativo, com um sufixo. Não use apenas Intents como sufixo porque esse nome se tornará o nome do seu módulo no Swift e entrará em conflito com os Intents reais.
    Uma captura de tela da tela do desenvolvedor da Apple para criar um ID de aplicativo
    Crie um ID de aplicativo para a extensão Intents (visualização grande)
  2. Ative este ID de aplicativo também para grupos de aplicativos (e configure o grupo como fizemos antes).

Agora, crie um perfil de provisionamento de desenvolvimento para a extensão Intents e gere novamente o perfil de provisionamento do seu aplicativo. Faça o download e instale-os como faria normalmente.

Agora que nossos perfis estão instalados, precisamos ir ao Xcode e atualizar os direitos do aplicativo.

Atualizando os direitos do seu aplicativo no Xcode

De volta ao Xcode, escolha o nome do seu projeto no navegador do projeto. Em seguida, escolha o alvo principal do seu aplicativo e vá para a guia “Recursos”. Lá, você verá uma opção para ativar o suporte à Siri.

Uma captura de tela da tela de direitos do Xcode mostrando que o SiriKit está ativado
Ative o SiriKit nos direitos do seu aplicativo. (Visualização grande)

Mais abaixo na lista, você pode ativar os grupos de aplicativos e configurá-lo.

Uma captura de tela da tela de direitos do Xcode mostrando que o grupo de aplicativos está ativado e configurado
Configurar o grupo de aplicativos do aplicativo (visualização grande)

Se você configurou corretamente, verá isso no arquivo .entitlements do seu aplicativo:

Uma captura de tela da plist do aplicativo mostrando que os direitos estão definidos
A plist mostra os direitos que você definiu (Visualização grande)

Agora, finalmente estamos prontos para adicionar o destino da extensão Intents ao nosso projeto.

Adicionando a extensão de intents

Finalmente estamos prontos para adicionar a extensão. No Xcode, escolha “Arquivo” → “Novo destino”. Esta folha irá aparecer:

Uma captura de tela mostrando a extensão Intents na caixa de diálogo New Target no Xcode
Adicione a extensão Intents ao seu projeto (visualização grande)

Escolha “Extensão de intenções” e clique no botão “Avançar”. Preencha a tela a seguir:

Uma captura de tela do Xcode mostrando como você configura a extensão Intents
Configurar a extensão Intents (visualização grande)

O nome do produto precisa corresponder ao sufixo que você criou no ID do aplicativo de intents no site do desenvolvedor da Apple.

Estamos optando por não adicionar uma extensão de IU de intents. Isso não é abordado neste artigo, mas você pode adicioná-lo mais tarde, se precisar. Basicamente, é uma maneira de colocar sua própria marca e estilo de exibição nos resultados visuais da Siri.

Quando terminar, o Xcode criará uma classe de manipulador de intents que podemos usar como parte inicial para nossa implementação da Siri.

O manipulador de intenções: resolver, confirmar e lidar

O Xcode gerou um novo alvo que tem um ponto de partida para nós.

A primeira coisa que você precisa fazer é configurar esse novo destino para estar no mesmo grupo de aplicativos que o aplicativo. Como antes, vá para a guia “Recursos” do destino, ative os grupos de aplicativos e configure-o com o nome do seu grupo. Lembre-se de que os aplicativos do mesmo grupo têm um sandbox que podem ser usados ​​para compartilhar arquivos entre si. Precisamos disso para que as solicitações da Siri cheguem ao nosso aplicativo.

List-o-Mat tem uma função que retorna a pasta de documentos do grupo. Devemos usá-lo sempre que quisermos ler ou gravar em um arquivo compartilhado.

 func documentsFolder() -> URL? { return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.app-o-mat.ListOMat") }

Por exemplo, quando salvamos as listas, usamos isso:

 func save(lists: Lists) { guard let docsDir = documentsFolder() else { fatalError("no docs dir") } let url = docsDir.appendingPathComponent(fileName, isDirectory: false) // Encode lists as JSON and save to url }

O modelo de extensão Intents criou um arquivo chamado IntentHandler.swift , com uma classe chamada IntentHandler . Também o configurou para ser o ponto de entrada dos intents no plist da extensão.

Uma captura de tela do Xcode mostrando como o IntentHandler está configurado como um ponto de entrada
O plist de extensão de intenção configura o IntentHandler como o ponto de entrada

Neste mesmo plist , você verá uma seção para declarar as intenções que suportamos. Vamos começar com aquele que permite pesquisar listas, que se chama INSearchForNotebookItemsIntent . Adicione-o à matriz em IntentsSupported .

Uma captura de tela no Xcode mostrando que a extensão plist deve listar as intenções que ela lida
Adicione o nome da intenção à lista de intenções (visualização grande)

Agora, vá para IntentHandler.swift e substitua seu conteúdo por este código:

 import Intents class IntentHandler: INExtension { override func handler(for intent: INIntent) -> Any? { switch intent { case is INSearchForNotebookItemsIntent: return SearchItemsIntentHandler() default: return nil } } }

A função do handler é chamada para obter um objeto para manipular uma intenção específica. Você pode simplesmente implementar todos os protocolos nesta classe e retornar self , mas colocaremos cada intent em sua própria classe para mantê-la melhor organizada.

Como pretendemos ter algumas classes diferentes, vamos dar a elas uma classe base comum para o código que precisamos compartilhar entre elas:

 class ListOMatIntentsHandler: NSObject { }

A estrutura de intents exige que herdemos de NSObject . Vamos preencher alguns métodos mais tarde.

Começamos nossa implementação de pesquisa com isso:

 class SearchItemsIntentHandler: ListOMatIntentsHandler, INSearchForNotebookItemsIntentHandling { }

Para definir um manipulador de intent, precisamos implementar três etapas básicas

  1. Resolva os parâmetros.
    Certifique-se de que os parâmetros necessários sejam fornecidos e elimine qualquer ambiguidade que você não entenda completamente.
  2. Confirme se a solicitação é factível.
    Isso geralmente é opcional, mas mesmo que você saiba que cada parâmetro é bom, ainda pode precisar de acesso a um recurso externo ou ter outros requisitos.
  3. Trate o pedido.
    Faça o que está sendo solicitado.

INSearchForNotebookItemsIntent , o primeiro intent que implementaremos, pode ser usado como uma pesquisa de tarefa. Os tipos de solicitações que podemos lidar com isso são: “No List-o-Mat, mostre a lista de supermercados” ou “No List-o-Mat, mostre a lista de lojas”.

A parte: “List-o-Mat” é na verdade um nome ruim para um aplicativo SiriKit porque o Siri tem dificuldade com hífens em aplicativos. Felizmente, o SiriKit nos permite ter nomes alternativos e fornecer pronúncia. No Info.plist do aplicativo, adicione esta seção:

Uma captura de tela do Xcode mostrando que a lista de aplicativos pode adicionar nomes e pronúncias de aplicativos alternativos
Adicione nomes de aplicativos alternativos e guias de pronúncia à lista de aplicativos

Isso permite que o usuário diga “list oh mat” e que isso seja entendido como uma única palavra (sem hífens). Não parece ideal na tela, mas sem ela, a Siri às vezes pensa que “Lista” e “Mat” são palavras separadas e fica muito confusa.

Resolver: Descobrindo os Parâmetros

Para uma pesquisa de itens do notebook, existem vários parâmetros:

  1. o tipo de item (uma tarefa, uma lista de tarefas ou uma nota),
  2. o título do item,
  3. o conteúdo do item,
  4. o status de conclusão (se a tarefa está marcada como concluída ou não),
  5. o local a que está associado,
  6. a data a que está associado.

Exigimos apenas os dois primeiros, então precisaremos escrever funções de resolução para eles. INSearchForNotebookItemsIntent tem métodos para implementarmos.

Como nos preocupamos apenas em mostrar listas de tarefas, codificaremos isso na resolução para o tipo de item. Em SearchItemsIntentHandler , adicione isto:

 func resolveItemType(for intent: INSearchForNotebookItemsIntent, with completion: @escaping (INNotebookItemTypeResolutionResult) -> Void) { completion(.success(with: .taskList)) }

Portanto, não importa o que o usuário diga, estaremos procurando por listas de tarefas. Se quiséssemos expandir nosso suporte de pesquisa, deixaríamos a Siri tentar descobrir isso a partir da frase original e então usar completion(.needsValue()) se o tipo de item estivesse faltando. Alternativamente, podemos tentar adivinhar a partir do título vendo o que corresponde a ele. Nesse caso, concluiríamos com sucesso quando a Siri souber o que é, e completion(.notRequired()) quando formos tentar várias possibilidades.

A resolução do título é um pouco mais complicada. O que queremos é que a Siri use uma lista se encontrar uma com uma correspondência exata para o que você disse. Se não tiver certeza ou se houver mais de uma possibilidade, queremos que a Siri nos peça ajuda para descobrir. Para fazer isso, o SiriKit fornece um conjunto de enumerações de resolução que nos permitem expressar o que queremos que aconteça em seguida.

Então, se você disser "Mercery", a Siri teria uma correspondência exata. Mas se você disser "Loja", a Siri apresentaria um menu de listas correspondentes.

Vamos começar com esta função para dar a estrutura básica:

 func resolveTitle(for intent: INSearchForNotebookItemsIntent, with completion: @escaping (INSpeakableStringResolutionResult) -> Void) { guard let title = intent.title else { completion(.needsValue()) return } let possibleLists = getPossibleLists(for: title) completeResolveListName(with: possibleLists, for: title, with: completion) }

Implementaremos getPossibleLists(for:) e completeResolveListName(with:for:with:) na classe base ListOMatIntentsHandler .

getPossibleLists(for:) precisa tentar combinar o título que a Siri nos passa com os nomes reais da lista.

 public func getPossibleLists(for listName: INSpeakableString) -> [INSpeakableString] { var possibleLists = [INSpeakableString]() for l in loadLists() { if l.name.lowercased() == listName.spokenPhrase.lowercased() { return [INSpeakableString(spokenPhrase: l.name)] } if l.name.lowercased().contains(listName.spokenPhrase.lowercased()) || listName.spokenPhrase.lowercased() == "all" { possibleLists.append(INSpeakableString(spokenPhrase: l.name)) } } return possibleLists }

Percorremos todas as nossas listas. Se obtivermos uma correspondência exata, a retornaremos e, caso contrário, retornaremos uma matriz de possibilidades. Nesta função, estamos simplesmente verificando se a palavra que o usuário disse está contida em um nome de lista (portanto, uma correspondência bastante simples). Isso permite que “Grocery” corresponda a “Grocery Store”. Um algoritmo mais avançado pode tentar corresponder com base em palavras que soam iguais (por exemplo, com o algoritmo Soundex),

completeResolveListName(with:for:with:) é responsável por decidir o que fazer com esta lista de possibilidades.

 public func completeResolveListName(with possibleLists: [INSpeakableString], for listName: INSpeakableString, with completion: @escaping (INSpeakableStringResolutionResult) -> Void) { switch possibleLists.count { case 0: completion(.unsupported()) case 1: if possibleLists[0].spokenPhrase.lowercased() == listName.spokenPhrase.lowercased() { completion(.success(with: possibleLists[0])) } else { completion(.confirmationRequired(with: possibleLists[0])) } default: completion(.disambiguation(with: possibleLists)) } }

Se obtivermos uma correspondência exata, informamos à Siri que conseguimos. Se obtivermos uma correspondência inexata, pedimos à Siri para perguntar ao usuário se acertou.

Se obtivermos várias correspondências, usamos completion(.disambiguation(with: possibleLists)) para dizer à Siri para mostrar uma lista e deixar o usuário escolher uma.

Agora que sabemos qual é a solicitação, precisamos analisar a coisa toda e ter certeza de que podemos lidar com isso.

Confirme: Verifique todas as suas dependências

Nesse caso, se tivermos resolvido todos os parâmetros, sempre poderemos tratar a solicitação. As implementações típicas de confirm() podem verificar a disponibilidade de serviços externos ou verificar os níveis de autorização.

Como confirm() é opcional, não poderíamos fazer nada, e a Siri assumiria que poderíamos lidar com qualquer solicitação com parâmetros resolvidos. Para ser explícito, poderíamos usar isso:

 func confirm(intent: INSearchForNotebookItemsIntent, completion: @escaping (INSearchForNotebookItemsIntentResponse) -> Void) { completion(INSearchForNotebookItemsIntentResponse(code: .success, userActivity: nil)) }

Isso significa que podemos lidar com qualquer coisa.

Manipular: Faça

A etapa final é lidar com a solicitação.

 func handle(intent: INSearchForNotebookItemsIntent, completion: @escaping (INSearchForNotebookItemsIntentResponse) -> Void) { guard let title = intent.title, let list = loadLists().filter({ $0.name.lowercased() == title.spokenPhrase.lowercased()}).first else { completion(INSearchForNotebookItemsIntentResponse(code: .failure, userActivity: nil)) return } let response = INSearchForNotebookItemsIntentResponse(code: .success, userActivity: nil) response.tasks = list.items.map { return INTask(title: INSpeakableString(spokenPhrase: $0.name), status: $0.done ? INTaskStatus.completed : INTaskStatus.notCompleted, taskType: INTaskType.notCompletable, spatialEventTrigger: nil, temporalEventTrigger: nil, createdDateComponents: nil, modifiedDateComponents: nil, identifier: "\(list.name)\t\($0.name)") } completion(response) }

Primeiro, encontramos a lista com base no título. Neste ponto, resolveTitle já garantiu que obteremos uma correspondência exata. Mas se houver um problema, ainda podemos retornar uma falha.

Quando temos uma falha, temos a opção de passar uma atividade do usuário. Se seu aplicativo usa Handoff e tem uma maneira de lidar com esse tipo exato de solicitação, a Siri pode tentar adiar seu aplicativo para tentar a solicitação lá. Ele não fará isso quando estivermos em um contexto somente de voz (por exemplo, você começou com “Hey Siri”), e não garante que fará isso em outros casos, então não conte com isso.

Isso agora está pronto para testar. Escolha a extensão de intenção na lista de destino no Xcode. Mas antes de executá-lo, edite o esquema.

Uma captura de tela do Xcode mostrando como editar um esquema
Edite o esquema da intenção para adicionar uma frase de amostra para depuração.

Isso traz uma maneira de fornecer uma consulta diretamente:

Uma captura de tela do Xcode mostrando a caixa de diálogo do esquema de edição
Adicione a frase de amostra à seção Executar do esquema. (Visualização grande)

Observe que estou usando “ListOMat” por causa do problema de hífens mencionado acima. Felizmente, é pronunciado da mesma forma que o nome do meu aplicativo, então não deve ser um grande problema.

De volta ao aplicativo, fiz uma lista de “mercearia” e uma lista de “loja de hardware”. Se eu pedir à Siri a lista de “loja”, ela passará pelo caminho de desambiguação, que se parece com isso:

Um GIF animado mostrando a Siri processando uma solicitação para mostrar a lista de lojas
A Siri lida com a solicitação pedindo esclarecimentos. (Visualização grande)

Se você disser "Mercearia", obterá uma correspondência exata, que vai direto para os resultados.

Adicionando itens via Siri

Agora que conhecemos os conceitos básicos de resolver, confirmar e manipular, podemos adicionar rapidamente uma intenção para adicionar um item a uma lista.

Primeiro, adicione INAddTasksIntent à plist da extensão:

Uma captura de tela no XCode mostrando a nova intenção sendo adicionada à plist
Adicione o INAddTasksIntent à extensão plist (visualização grande)

Em seguida, atualize a função handle do nosso IntentHandler .

 override func handler(for intent: INIntent) -> Any? { switch intent { case is INSearchForNotebookItemsIntent: return SearchItemsIntentHandler() case is INAddTasksIntent: return AddItemsIntentHandler() default: return nil } }

Adicione um stub para a nova classe:

 class AddItemsIntentHandler: ListOMatIntentsHandler, INAddTasksIntentHandling { }

Adicionar um item precisa de uma resolve semelhante para pesquisa, exceto com uma lista de tarefas de destino em vez de um título.

 func resolveTargetTaskList(for intent: INAddTasksIntent, with completion: @escaping (INTaskListResolutionResult) -> Void) { guard let title = intent.targetTaskList?.title else { completion(.needsValue()) return } let possibleLists = getPossibleLists(for: title) completeResolveTaskList(with: possibleLists, for: title, with: completion) }

completeResolveTaskList é como completeResolveListName , mas com tipos ligeiramente diferentes (uma lista de tarefas em vez do título de uma lista de tarefas).

 public func completeResolveTaskList(with possibleLists: [INSpeakableString], for listName: INSpeakableString, with completion: @escaping (INTaskListResolutionResult) -> Void) { let taskLists = possibleLists.map { return INTaskList(title: $0, tasks: [], groupName: nil, createdDateComponents: nil, modifiedDateComponents: nil, identifier: nil) } switch possibleLists.count { case 0: completion(.unsupported()) case 1: if possibleLists[0].spokenPhrase.lowercased() == listName.spokenPhrase.lowercased() { completion(.success(with: taskLists[0])) } else { completion(.confirmationRequired(with: taskLists[0])) } default: completion(.disambiguation(with: taskLists)) } }

Ele tem a mesma lógica de desambiguação e se comporta exatamente da mesma maneira. Dizer "Loja" precisa ser desambiguado e dizer "Mercearia" seria uma correspondência exata.

Deixaremos a confirm não implementada e aceitaremos o padrão. Para handle , precisamos adicionar um item à lista e salvá-lo.

 func handle(intent: INAddTasksIntent, completion: @escaping (INAddTasksIntentResponse) -> Void) { var lists = loadLists() guard let taskList = intent.targetTaskList, let listIndex = lists.index(where: { $0.name.lowercased() == taskList.title.spokenPhrase.lowercased() }), let itemNames = intent.taskTitles, itemNames.count > 0 else { completion(INAddTasksIntentResponse(code: .failure, userActivity: nil)) return } // Get the list var list = lists[listIndex] // Add the items var addedTasks = [INTask]() for item in itemNames { list.addItem(name: item.spokenPhrase, at: list.items.count) addedTasks.append(INTask(title: item, status: .notCompleted, taskType: .notCompletable, spatialEventTrigger: nil, temporalEventTrigger: nil, createdDateComponents: nil, modifiedDateComponents: nil, identifier: nil)) } // Save the new list lists[listIndex] = list save(lists: lists) // Respond with the added items let response = INAddTasksIntentResponse(code: .success, userActivity: nil) response.addedTasks = addedTasks completion(response) }

Obtemos uma lista de itens e uma lista de destino. Procuramos a lista e adicionamos os itens. Também precisamos preparar uma resposta para a Siri mostrar com os itens adicionados e enviá-la para a função de conclusão.

Essa função pode lidar com uma frase como “Em ListOMat, adicione maçãs à lista de compras”. Ele também pode lidar com uma lista de itens como “arroz, cebola e azeitonas”.

Uma captura de tela do simulador mostrando a Siri adicionando itens à lista do supermercado
Siri adiciona alguns itens à lista do supermercado

Quase pronto, apenas mais algumas configurações

Tudo isso funcionará em seu simulador ou dispositivo local, mas se você quiser enviar isso, precisará adicionar uma chave NSSiriUsageDescription ao plist do seu aplicativo, com uma string que descreve para que você está usando a Siri. Algo como “Suas solicitações sobre listas serão enviadas para a Siri” está bem.

Você também deve adicionar uma chamada para:

 INPreferences.requestSiriAuthorization { (status) in }

Coloque isso no viewDidLoad do seu controlador de exibição principal para solicitar ao usuário acesso à Siri. This will show the message you configured above and also let the user know that they could be using Siri for this app.

A screenshot of the dialog that a device pops up when you ask for Siri permission
The device will ask for permission if you try to use Siri in the app.

Finally, you'll need to tell Siri what to tell the user if the user asks what your app can do, by providing some sample phrases:

  1. Create a plist file in your app (not the extension), named AppIntentVocabulary.plist .
  2. Fill out the intents and phrases that you support.
A screenshot of the AppIntentVocabulary.plist showing sample phrases
Add an AppIntentVocabulary.plist to list the sample phrases that will invoke the intent you handle. (Visualização grande)

There is no way to really know all of the phrases that Siri will use for an intent, but Apple does provide a few samples for each intent in its documentation. The sample phrases for task-list searching show us that Siri can understand “Show me all my notes on <appName>,” but I found other phrases by trial and error (for example, Siri understands what “lists” are too, not just notes).

Resumo

As you can see, adding Siri support to an app has a lot of steps, with a lot of configuration. But the code needed to handle the requests was fairly simple.

There are a lot of steps, but each one is small, and you might be familiar with a few of them if you have used extensions before.

Here is what you'll need to prepare for a new extension on Apple's developer website:

  1. Make an app ID for an Intents extension.
  2. Make an app group if you don't already have one.
  3. Use the app group in the app ID for the app and extension.
  4. Add Siri support to the app's ID.
  5. Regenerate the profiles and download them.

And here are the steps in Xcode for creating Siri's Intents extension:

  1. Add an Intents extension using the Xcode template.
  2. Update the entitlements of the app and extension to match the profiles (groups and Siri support).
  3. Add your intents to the extension's plist .

And you'll need to add code to do the following things:

  1. Use the app group sandbox to communicate between the app and extension.
  2. Add classes to support each intent with resolve, confirm and handle functions.
  3. Update the generated IntentHandler to use those classes.
  4. Ask for Siri access somewhere in your app.

Finally, there are some Siri-specific configuration settings:

  1. Add the Siri support security string to your app's plist .
  2. Add sample phrases to an AppIntentVocabulary.plist file in your app.
  3. Run the intent target to test; edit the scheme to provide the phrase.

OK, that is a lot, but if your app fits one of Siri's domains, then users will expect that they can interact with it via voice. And because the competition for voice assistants is so good, we can only expect that WWDC 2018 will bring a bunch more domains and, hopefully, much better Siri.

Leitura adicional

  • “SiriKit,” Apple
    The technical documentation contains the full list of domains and intents.
  • “Guides and Sample Code,” Apple
    Includes code for many domains.
  • “Introducing SiriKit” (video, Safari only), WWDC 2016 Apple
  • “What's New in SiriKit” (video, Safari only), WWDC 2017, Apple
    Apple introduces lists and notes
  • “Lists and Notes,” Apple
    The full list of lists and notes intents.