Подойдут ли намерения SiriKit вашему приложению? Если да, то вот как их использовать

Опубликовано: 2022-03-10
Краткий обзор ↬ С прошлого года появилась возможность добавить поддержку Siri в приложение, если оно соответствует одному из заранее определенных вариантов использования Apple. Узнайте, подойдет ли вам SiriKit и как его использовать.

Начиная с iOS 5 Siri помогает пользователям iPhone отправлять сообщения, устанавливать напоминания и искать рестораны с помощью приложений Apple. Начиная с iOS 10, мы также можем использовать Siri в некоторых наших собственных приложениях.

Чтобы использовать эту функцию, ваше приложение должно соответствовать предопределенным Apple «доменам и намерениям» Siri. В этой статье мы узнаем, что это такое, и посмотрим, могут ли наши приложения их использовать. Мы возьмем простое приложение, представляющее собой менеджер списков дел, и узнаем, как добавить поддержку Siri. Мы также рассмотрим рекомендации веб-сайта разработчиков Apple по настройке и коду Swift для нового типа расширения, которое было представлено вместе с SiriKit: расширение Intents .

Когда вы доберетесь до части этой статьи, посвященной кодированию, вам понадобится Xcode (по крайней мере версии 9.x), и было бы хорошо, если бы вы были знакомы с разработкой iOS в Swift, потому что мы собираемся добавить Siri в небольшой рабочий приложение. Мы рассмотрим этапы настройки расширения на веб-сайте Apple для разработчиков и добавления кода расширения Siri в приложение.

«Привет, Сири, зачем ты мне?»

Иногда я пользуюсь телефоном, лежа на диване, обе руки свободны, и я могу полностью сосредоточиться на экране. Может быть, я напишу своей сестре, чтобы спланировать день рождения нашей мамы, или отвечу на вопрос в Trello. Я вижу приложение. Я могу коснуться экрана. Я могу печатать.

Но я могу гулять по своему городу, слушать подкаст, когда на мои часы приходит сообщение. Мой телефон в кармане, и я не могу легко ответить на ходу.

Еще после прыжка! Продолжить чтение ниже ↓

С помощью Siri я могу, удерживая кнопку управления наушниками, сказать: «Напиши сестре, что я буду там к двум часам». Siri великолепна, когда вы в пути и не можете полностью сосредоточиться на своем телефоне или когда взаимодействие незначительное, но для этого требуется несколько нажатий и набор текста.

Это нормально, если я хочу использовать приложения Apple для этих взаимодействий. Но у некоторых категорий приложений, таких как обмен сообщениями, есть очень популярные альтернативы. Другие действия, такие как заказ поездки или столик в ресторане, даже не возможны со встроенными приложениями Apple, но идеально подходят для Siri.

Подход Apple к голосовым помощникам

Чтобы активировать Siri в сторонних приложениях, Apple пришлось выбрать механизм, позволяющий брать звук из голоса пользователя и каким-то образом передавать его в приложение таким образом, чтобы оно могло выполнить запрос. Чтобы сделать это возможным, Apple требует, чтобы пользователь упомянул имя приложения в запросе, но у них было несколько вариантов того, что делать с остальной частью запроса.

  • Он мог отправить звуковой файл в приложение.
    Преимущество такого подхода заключается в том, что приложение может попытаться обработать буквально любой запрос, который пользователь может сделать для него. Такой подход мог бы понравиться Amazon или Google, потому что у них уже есть сложные сервисы распознавания голоса. Но большинство приложений не смогут справиться с этим очень легко.
  • Он мог превратить речь в текст и отправить его.
    Поскольку многие приложения не имеют сложных реализаций на естественном языке, пользователю обычно приходится придерживаться очень определенных фраз, а поддержка неанглоязычных языков должна быть реализована разработчиком приложения.
  • Он мог бы попросить вас предоставить список фраз, которые вы понимаете.
    Этот механизм ближе к тому, что Amazon делает с Alexa (в его структуре «навыков»), и он позволяет использовать Alexa гораздо больше, чем SiriKit может в настоящее время обрабатывать. В навыке Alexa вы предоставляете фразы с переменными-заполнителями, которые Alexa заполнит для вас. Например, «Alexa, напомни мне в $TIME$ на $REMINDER$ » — Alexa будет сравнивать эту фразу с тем, что сказал пользователь, и сообщит вам значения для TIME и REMINDER . Как и в случае с предыдущим механизмом, разработчик должен сделать весь перевод, и нет большой гибкости, если пользователь говорит что-то немного другое.
  • Он может определить список запросов с параметрами и отправить приложению структурированный запрос.
    На самом деле это то, что делает Apple, и преимущество заключается в том, что она может поддерживать множество языков и делает всю работу, чтобы попытаться понять все способы, которыми пользователь может сформулировать запрос. Большим недостатком является то, что вы можете реализовать обработчики только для запросов, которые определяет Apple. Это замечательно, если у вас есть, например, приложение для обмена сообщениями, но если у вас есть служба потоковой передачи музыки или проигрыватель подкастов, у вас сейчас нет возможности использовать SiriKit.

Точно так же приложения могут общаться с пользователем тремя способами: с помощью звука, с помощью текста, который преобразуется, или выражая то, что вы хотите сказать, и позволяя системе определить точный способ выразить это. Последнее решение (именно это и делает Apple) возлагает на Apple бремя перевода, но дает вам ограниченные возможности использования собственных слов для описания вещей.

Типы запросов, которые вы можете обрабатывать, определяются в доменах и намерениях SiriKit. Намерение — это тип запроса, который может сделать пользователь, например, отправить текстовое сообщение контакту или найти фотографию. Каждое намерение имеет список параметров — например, для отправки текстовых сообщений требуется контакт и сообщение.

Домен — это просто группа связанных намерений. Чтение текста и отправка текста находятся в домене обмена сообщениями. Бронирование поездки и получение местоположения находятся в домене бронирования поездок. Есть домены для совершения VoIP-звонков, начала тренировок, поиска фотографий и еще нескольких вещей. Документация SiriKit содержит полный список доменов и их намерений.

Общая критика Siri заключается в том, что она, похоже, не может обрабатывать запросы так же хорошо, как Google и Alexa, и что сторонняя голосовая экосистема, созданная конкурентами Apple, богаче.

Я согласен с этими критическими замечаниями. Если ваше приложение не соответствует текущим намерениям, вы не можете использовать SiriKit и ничего не можете сделать. Даже если ваше приложение подходит, вы не можете контролировать все слова, которые Siri говорит или понимает; поэтому, если у вас есть особый способ говорить о вещах в вашем приложении, вы не всегда можете научить Siri этому.

Разработчики iOS надеются, что Apple значительно расширит свой список намерений и что обработка естественного языка станет намного лучше. Если это произойдет, то у нас будет голосовой помощник, который работает без необходимости разработчикам переводить или понимать все способы сказать одно и то же. А реализовать поддержку структурированных запросов на самом деле довольно просто — намного проще, чем создавать анализатор естественного языка.

Еще одно большое преимущество структуры намерений заключается в том, что она не ограничивается Siri и голосовыми запросами. Даже сейчас приложение «Карты» может генерировать запрос вашего приложения на основе намерений (например, бронирование столика в ресторане). Он делает это программно (не голосом или естественным языком). Если бы Apple разрешила приложениям обнаруживать открытые намерения друг друга, у нас был бы гораздо лучший способ совместной работы приложений (в отличие от URL-адресов в стиле x-обратного вызова).

Наконец, поскольку намерение представляет собой структурированный запрос с параметрами, у приложения есть простой способ указать, что параметры отсутствуют или что ему нужна помощь в различении некоторых параметров. Затем Siri может задавать дополнительные вопросы для разрешения параметров без необходимости приложению вести разговор.

Домен бронирования поездок

Чтобы понять домены и намерения, давайте посмотрим на домен бронирования поездок. Это домен, который вы бы использовали, чтобы попросить Siri достать вам автомобиль Lyft.

Apple определяет, как попросить подвезти и как получить информацию об этом, но на самом деле нет встроенного приложения Apple, которое могло бы обработать этот запрос. Это один из немногих доменов, где требуется приложение с поддержкой SiriKit.

Вы можете вызвать одно из намерений с помощью голоса или непосредственно из Карт. Некоторые намерения для этого домена:

  • Заказать поездку
    Воспользуйтесь этим, чтобы заказать поездку. Вам нужно будет указать место посадки и высадки, а приложению также может потребоваться знать размер вашей группы и тип поездки, которую вы хотите. Примером фразы может быть: «Закажи мне поездку с <appname>».
  • Получить статус поездки
    Используйте это намерение, чтобы узнать, был ли получен ваш запрос, и получить информацию о транспортном средстве и водителе, включая их местонахождение. Приложение «Карты» использует это намерение, чтобы показать обновленное изображение автомобиля, когда он приближается к вам.
  • Отменить поездку
    Используйте это, чтобы отменить поездку, которую вы забронировали.

Для любого из этих намерений Siri может потребоваться дополнительная информация. Как вы увидите, когда мы реализуем обработчик намерений, ваше расширение Intents может сообщить Siri, что требуемый параметр отсутствует, и Siri запросит его у пользователя.

Тот факт, что Карты могут вызывать намерения программно, показывает, как намерения могут обеспечить взаимодействие между приложениями в будущем.

Примечание . Вы можете получить полный список доменов и их назначения на веб-сайте Apple для разработчиков. Существует также пример приложения Apple со многими реализованными доменами и намерениями, включая бронирование поездок.

Добавление поддержки домена списков и заметок в ваше приложение

Хорошо, теперь, когда мы познакомились с основами SiriKit, давайте посмотрим, как вы можете добавить поддержку Siri в приложение, которое включает в себя множество настроек и класс для каждого намерения, которое вы хотите обработать.

Остальная часть этой статьи состоит из подробных шагов по добавлению поддержки Siri в приложение. Вам нужно сделать пять высокоуровневых вещей:

  1. Подготовьтесь к добавлению нового расширения в приложение, создав профили подготовки с новыми правами для него на веб-сайте Apple для разработчиков.
  2. Настройте свое приложение (через его plist ) для использования прав.
  3. Используйте шаблон Xcode, чтобы начать работу с некоторым примером кода.
  4. Добавьте код, подтверждающий ваше намерение Siri.
  5. Настройте словарь Siri с помощью plist s.

Не волнуйтесь: мы рассмотрим каждый из них, объясняя расширения и права по пути.

Чтобы сосредоточиться только на частях Siri, я подготовил простой менеджер списков дел List-o-Mat.

Анимированный GIF-файл с демонстрацией List-o-Mat.
Создание списков в List-o-Mat (большой превью)

Вы можете найти полный исходный код примера List-o-Mat на GitHub.

Чтобы создать его, все, что я сделал, это начал с шаблона приложения Xcode Master-Detail и превратил оба экрана в UITableView . Я добавил способ добавления и удаления списков и элементов, а также способ отмечать элементы как выполненные. Вся навигация создается шаблоном.

Для хранения данных я использовал протокол Codable (представленный на WWDC 2017), который преобразует структуры в JSON и сохраняет их в текстовом файле в папке documents .

Я намеренно сделал код очень простым. Если у вас есть опыт работы со Swift и создания контроллеров представлений, у вас не должно возникнуть с этим проблем.

Теперь мы можем пройти этапы добавления поддержки SiriKit. Шаги высокого уровня будут одинаковыми для любого приложения и любого домена и намерений, которые вы планируете реализовать. В основном мы будем иметь дело с веб-сайтом для разработчиков Apple, редактировать plist и немного писать на Swift.

Для List-o-Mat мы сосредоточимся на домене списков и заметок, который широко применим к таким вещам, как приложения для создания заметок и списки дел.

В домене списков и заметок у нас есть следующие намерения, которые имеют смысл для нашего приложения.

  • Получить список задач.
  • Добавить новую задачу в список.

Поскольку взаимодействие с Siri на самом деле происходит за пределами вашего приложения (возможно, даже когда ваше приложение не запущено), iOS использует расширение для реализации этого.

Расширение намерений

Если вы никогда не работали с расширениями, вам необходимо знать три основных момента:

  1. Расширение — это отдельный процесс. Он поставляется внутри пакета вашего приложения, но работает полностью сам по себе, в собственной изолированной программной среде.
  2. Ваше приложение и расширение могут взаимодействовать друг с другом, находясь в одной группе приложений. Самый простой способ — через общие папки песочницы группы (таким образом, они могут читать и писать в одни и те же файлы, если вы поместите их туда).
  3. Для расширений требуются собственные идентификаторы приложений, профили и права.

Чтобы добавить расширение в свое приложение, войдите в свою учетную запись разработчика и перейдите в раздел «Сертификаты, идентификаторы и профили».

Обновление данных учетной записи приложения Apple Developer

В нашей учетной записи разработчика Apple первое, что нам нужно сделать, это создать группу приложений. Перейдите в раздел «Группы приложений» в разделе «Идентификаторы» и добавьте его.

Скриншот диалогового окна веб-сайта разработчика Apple для регистрации группы приложений
Регистрация группы приложений (большой предварительный просмотр)

Он должен начинаться с group , за которой следует ваш обычный обратный идентификатор на основе домена. Поскольку у него есть префикс, вы можете использовать идентификатор вашего приложения для остальных.

Затем нам нужно обновить идентификатор нашего приложения, чтобы использовать эту группу и включить Siri:

  1. Перейдите в раздел «Идентификаторы приложений» и нажмите на идентификатор вашего приложения;
  2. Нажмите кнопку «Редактировать»;
  3. Включить группы приложений (если они не включены для другого расширения).
    Скриншот веб-сайта разработчика Apple, позволяющий использовать группы приложений для идентификатора приложения.
    Включить группы приложений (большой предварительный просмотр)
  4. Затем настройте группу приложений, нажав кнопку «Изменить». Выберите группу приложений из предыдущего.
    Снимок экрана диалогового окна веб-сайта разработчика Apple для установки имени группы приложений.
    Установите имя группы приложений (большой предварительный просмотр)
  5. Включите SiriKit.
    Скриншот с включенным SiriKit
    Включить SiriKit (большой предварительный просмотр)
  6. Нажмите «Готово», чтобы сохранить его.

Теперь нам нужно создать новый идентификатор приложения для нашего расширения:

  1. В том же разделе «Идентификаторы приложений» добавьте новый идентификатор приложения. Это будет идентификатор вашего приложения с суффиксом. Не используйте только Intents в качестве суффикса, потому что это имя станет именем вашего модуля в Swift и будет конфликтовать с реальными Intents .
    Скриншот экрана разработчика Apple для создания идентификатора приложения
    Создайте идентификатор приложения для расширения Intents (большой предварительный просмотр)
  2. Включите этот идентификатор приложения и для групп приложений (и настройте группу, как мы делали ранее).

Теперь создайте профиль подготовки разработки для расширения Intents и повторно создайте профиль подготовки вашего приложения. Загрузите и установите их, как обычно.

Теперь, когда наши профили установлены, нам нужно перейти в Xcode и обновить права приложения.

Обновление прав вашего приложения в Xcode

Вернувшись в Xcode, выберите имя своего проекта в навигаторе проектов. Затем выберите основную цель вашего приложения и перейдите на вкладку «Возможности». Там вы увидите переключатель для включения поддержки Siri.

Скриншот экрана прав Xcode, показывающий, что SiriKit включен
Включите SiriKit в правах вашего приложения. (Большой превью)

Далее по списку вы можете включить группы приложений и настроить их.

Снимок экрана с правами Xcode, показывающий, что группа приложений включена и настроена.
Настройка группы приложений приложения (большой предварительный просмотр)

Если вы настроили его правильно, вы увидите это в файле .entitlements вашего приложения:

Скриншот plist приложения, показывающий, что права установлены
Список показывает права, которые вы установили (большой предварительный просмотр)

Теперь мы, наконец, готовы добавить цель расширения Intents в наш проект.

Добавление расширения намерений

Наконец-то мы готовы добавить расширение. В Xcode выберите «Файл» → «Новая цель». Этот лист появится:

Снимок экрана, показывающий расширение Intents в диалоговом окне «Новая цель» в Xcode.
Добавьте расширение Intents в свой проект (большой предварительный просмотр)

Выберите «Расширение намерений» и нажмите кнопку «Далее». Заполните следующий экран:

Снимок экрана из Xcode, показывающий, как вы настраиваете расширение Intents.
Настройте расширение Intents (большой предварительный просмотр)

Название продукта должно соответствовать суффиксу, указанному вами в идентификаторе приложения намерений на веб-сайте разработчика Apple.

Мы решили не добавлять расширение пользовательского интерфейса намерений. Это не рассматривается в этой статье, но вы можете добавить его позже, если вам это нужно. По сути, это способ добавить свой собственный брендинг и стиль отображения в визуальные результаты Siri.

Когда вы закончите, Xcode создаст класс обработчика намерений, который мы можем использовать в качестве начальной части для нашей реализации Siri.

Обработчик намерений: решить, подтвердить и обработать

Xcode сгенерировал новую цель, которая имеет для нас отправную точку.

Первое, что вам нужно сделать, это настроить эту новую цель так, чтобы она находилась в той же группе приложений, что и приложение. Как и раньше, перейдите на вкладку «Возможности» цели, включите группы приложений и настройте их с помощью имени вашей группы. Помните, что у приложений в одной группе есть песочница, которую они могут использовать для обмена файлами друг с другом. Это нужно для того, чтобы запросы Siri попадали в наше приложение.

List-o-Mat имеет функцию, которая возвращает папку группового документа. Мы должны использовать его всякий раз, когда мы хотим читать или писать в общий файл.

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

Например, когда мы сохраняем списки, мы используем это:

 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 }

Шаблон расширения Intents создал файл с именем IntentHandler.swift с классом с именем IntentHandler . Он также настроил его как точку входа намерений в plist расширения.

Снимок экрана из Xcode, показывающий, как IntentHandler настроен как точка входа.
Plist расширения намерений настраивает IntentHandler как точку входа.

В этом же plist вы увидите раздел, в котором объявляются намерения, которые мы поддерживаем. Мы собираемся начать с того, который позволяет искать списки, который называется INSearchForNotebookItemsIntent . Добавьте его в массив под IntentsSupported .

Снимок экрана в Xcode, показывающий, что список расширений должен перечислять намерения, которые он обрабатывает.
Добавьте имя намерения в список намерений (большой предварительный просмотр)

Теперь перейдите к IntentHandler.swift и замените его содержимое следующим кодом:

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

Функция handler вызывается, чтобы заставить объект обрабатывать определенное намерение. Вы можете просто реализовать все протоколы в этом классе и вернуть self , но мы поместим каждое намерение в отдельный класс, чтобы лучше организовать его.

Поскольку мы намерены иметь несколько разных классов, давайте дадим им общий базовый класс для кода, который нам нужно разделить между ними:

 class ListOMatIntentsHandler: NSObject { }

Фреймворк намерений требует, чтобы мы наследовались от NSObject . Мы добавим некоторые методы позже.

Мы начинаем нашу реализацию поиска с этого:

 class SearchItemsIntentHandler: ListOMatIntentsHandler, INSearchForNotebookItemsIntentHandling { }

Чтобы установить обработчик намерений, нам нужно реализовать три основных шага.

  1. Решите параметры.
    Убедитесь, что заданы обязательные параметры, и устраните неоднозначность тех, которые вы не совсем понимаете.
  2. Подтвердите , что запрос выполним.
    Часто это необязательно, но даже если вы знаете, что каждый параметр хорош, вам все равно может понадобиться доступ к внешнему ресурсу или другие требования.
  3. Обработайте запрос.
    Делайте то, что просят.

INSearchForNotebookItemsIntent — первое намерение, которое мы реализуем, — может использоваться как поиск задач. Типы запросов, которые мы можем обрабатывать с помощью этого: «В List-o-Mat показать список продуктовых магазинов» или «В List-o-Mat показать список магазинов».

Кроме того, «List-o-Mat» на самом деле плохое имя для приложения SiriKit, потому что Siri тяжело справляется с дефисами в приложениях. К счастью, SiriKit позволяет нам использовать альтернативные имена и обеспечивать произношение. В Info.plist приложения добавьте этот раздел:

Снимок экрана из Xcode, показывающий, что список приложений может добавлять альтернативные имена приложений и произношения.
Добавьте альтернативные названия приложений и руководства по произношению в список приложений.

Это позволяет пользователю сказать «list oh mat» и понять это как одно слово (без дефисов). На экране это выглядит не идеально, но без него Сири иногда думает, что «Список» и «Мат» — это разные слова, и сильно путается.

Решение: выяснить параметры

Для поиска элементов блокнота есть несколько параметров:

  1. тип элемента (задача, список задач или заметка),
  2. название предмета,
  3. содержание предмета,
  4. статус завершения (независимо от того, отмечена ли задача как выполненная или нет),
  5. место, с которым оно связано,
  6. дата, с которой он связан.

Нам нужны только первые два, поэтому нам нужно написать для них функции разрешения. INSearchForNotebookItemsIntent есть методы, которые мы можем реализовать.

Поскольку нас интересует только отображение списков задач, мы жестко пропишем это в разрешении для типа элемента. В SearchItemsIntentHandler добавьте это:

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

Итак, независимо от того, что говорит пользователь, мы будем искать списки задач. Если бы мы хотели расширить нашу поддержку поиска, мы бы позволили Siri попытаться понять это из исходной фразы, а затем просто использовали completion(.needsValue()) , если тип элемента отсутствует. В качестве альтернативы мы могли бы попытаться угадать по названию, увидев, что ему соответствует. В этом случае мы бы завершили успешно, когда Сири знает, что это такое, и мы бы использовали completion(.notRequired()) , когда собираемся попробовать несколько возможностей.

Разрешение заголовка немного сложнее. Мы хотим, чтобы Siri использовала список, если найдет список с точным соответствием тому, что вы сказали. Если это не точно или существует более одной возможности, мы хотим, чтобы Сири попросила нас о помощи в выяснении этого. Для этого SiriKit предоставляет набор перечислений разрешения, которые позволяют нам выразить то, что мы хотим сделать дальше.

Так что, если вы скажете «Продуктовый магазин», то у Siri будет точное совпадение. Но если вы скажете «Магазин», то Siri представит меню из списков соответствия.

Мы начнем с этой функции, чтобы дать базовую структуру:

 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) }

Мы реализуем getPossibleLists(for:) и completeResolveListName(with:for:with:) в базовом классе ListOMatIntentsHandler .

getPossibleLists(for:) должен попытаться нечетко сопоставить заголовок, который Siri передает нам, с реальными именами списков.

 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 }

Мы перебираем все наши списки. Если мы получим точное совпадение, мы вернем его, а если нет, вернем массив возможностей. В этой функции мы просто проверяем, содержится ли слово, произнесенное пользователем, в имени списка (таким образом, довольно простое совпадение). Это позволяет «Продуктовым» соответствовать «Продовольственным магазинам». Более продвинутый алгоритм может попытаться сопоставить слова, которые звучат одинаково (например, с помощью алгоритма Soundex).

completeResolveListName(with:for:with:) отвечает за решение, что делать с этим списком возможностей.

 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)) } }

Если мы получили точное совпадение, мы говорим Siri, что нам это удалось. Если мы получили одно неточное совпадение, мы просим Siri спросить пользователя, правильно ли мы угадали.

Если мы получили несколько совпадений, то мы используем completion(.disambiguation(with: possibleLists)) чтобы указать Siri показать список и позволить пользователю выбрать один.

Теперь, когда мы знаем, что такое запрос, нам нужно посмотреть на него целиком и убедиться, что мы можем его обработать.

Подтвердить: проверьте все ваши зависимости

В этом случае, если мы разрешили все параметры, мы всегда можем обработать запрос. Типичные реализации confirm() могут проверять доступность внешних сервисов или проверять уровни авторизации.

Поскольку confirm() необязательно, мы могли просто ничего не делать, и Siri предположила бы, что мы можем обработать любой запрос с разрешенными параметрами. Чтобы быть явным, мы могли бы использовать это:

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

Это означает, что мы можем справиться с чем угодно.

Ручка: сделай это

Последним шагом является обработка запроса.

 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) }

Сначала мы находим список на основе заголовка. К этому моменту resolveTitle уже убедился, что мы получим точное совпадение. Но если есть проблема, мы все равно можем вернуть ошибку.

Когда у нас есть сбой, у нас есть возможность передать действие пользователя. Если ваше приложение использует Handoff и имеет способ обработки именно этого типа запроса, тогда Siri может попытаться отложить ваше приложение, чтобы попробовать запрос там. Он не будет этого делать, когда мы находимся в голосовом контексте (например, вы начали с «Привет, Siri»), и не гарантирует, что сделает это в других случаях, так что не рассчитывайте на это.

Теперь все готово для тестирования. Выберите расширение намерения в целевом списке в Xcode. Но прежде чем запускать, отредактируйте схему.

Скриншот из Xcode, показывающий, как редактировать схему
Отредактируйте схему намерения, чтобы добавить образец фразы для отладки.

Это приводит к способу предоставления запроса напрямую:

Снимок экрана из Xcode, показывающий диалоговое окно схемы редактирования
Добавьте образец фразы в раздел «Выполнить» схемы. (Большой превью)

Обратите внимание, я использую «ListOMat» из-за упомянутой выше проблемы с дефисами. К счастью, оно произносится так же, как и имя моего приложения, так что это не должно быть большой проблемой.

Вернувшись в приложение, я создал список «Продуктовый магазин» и список «Хозяйственный магазин». Если я попрошу у Siri список «магазинов», он пройдет путь устранения неоднозначности, который выглядит так:

Анимированный GIF-файл, показывающий, как Siri обрабатывает запрос на отображение списка магазинов.
Siri обрабатывает запрос, запрашивая разъяснения. (Большой превью)

Если вы скажете «Продовольственный магазин», вы получите точное совпадение, которое соответствует результатам.

Добавление элементов через Siri

Теперь, когда мы знаем основные понятия разрешения, подтверждения и обработки, мы можем быстро добавить намерение добавить элемент в список.

Сначала добавьте INAddTasksIntent в plist расширения:

Снимок экрана в XCode, показывающий, что новое намерение добавляется в plist.
Добавьте INAddTasksIntent в список расширений (большой предварительный просмотр)

Затем обновите функцию handle нашего IntentHandler .

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

Добавьте заглушку для нового класса:

 class AddItemsIntentHandler: ListOMatIntentsHandler, INAddTasksIntentHandling { }

Для добавления элемента требуется аналогичное resolve для поиска, за исключением того, что вместо заголовка используется целевой список задач.

 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 похож на completeResolveListName , но с немного другими типами (список задач вместо заголовка списка задач).

 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)) } }

Он имеет ту же логику устранения неоднозначности и ведет себя точно так же. Слово «Магазин» должно быть устранено, и слово «Продуктовый магазин» будет точным совпадением.

Мы оставим confirm нереализованным и примем значение по умолчанию. Для handle нам нужно добавить элемент в список и сохранить его.

 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) }

Мы получаем список элементов и целевой список. Мы просматриваем список и добавляем элементы. Нам также нужно подготовить ответ для Siri, который будет отображаться с добавленными элементами, и отправить его в функцию завершения.

Эта функция может обрабатывать такие фразы, как «В ListOMat добавьте яблоки в список покупок». Он также может обрабатывать список таких предметов, как «рис, лук и оливки».

Скриншот симулятора, показывающий, как Siri добавляет товары в список продуктового магазина.
Siri добавляет несколько товаров в список продуктового магазина

Почти готово, осталось еще несколько настроек

Все это будет работать в вашем симуляторе или на локальном устройстве, но если вы хотите отправить это, вам нужно добавить ключ NSSiriUsageDescription в plist вашего приложения со строкой, описывающей, для чего вы используете Siri. Что-то вроде «Ваши запросы о списках будут отправлены Siri» подойдет.

Вы также должны добавить вызов:

 INPreferences.requestSiriAuthorization { (status) in }

Поместите это в viewDidLoad контроллера основного представления, чтобы запросить у пользователя доступ к 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. (Большой превью)

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).

Резюме

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.

Дальнейшее чтение

  • “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.