Хабрахабр

Быстрые команды Siri

Одна из полезных (по моему мнению) фич iOS 12, представленных на WWDC 2018 — Siri Shortcuts.

shortcut) — быстрая команда, короткий способ совершить какое-либо действие в обход стандартному сценарию. Шорткат (англ.

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

Как это работает

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

Если мы нажмем на кнопку “More shortcuts”, то увидим все шорткаты, доставленные в систему каждым приложением. Посмотреть эти шорткаты можно в "Настройки → Siri и поиск".

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

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

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

К каждому шорткату мы можем добавить свою голосовую команду, если нажмем на иконку "+".

Получается, что чать функциональности вашего приложения пользователь сможет выполнять через Siri, не открывая само приложение. Шорткат с фразой сохранился в "My shortcuts". Произносим голосовую команду, нажимаем "Done", и теперь мы можем выполнять действие, стоящее за шорткатом, с помощью голоса через Siri.

Создание шорткатов

На момент написания статьи оба они на стадии Beta. Для разработки нам понадобятся XCode 10 и iOS 12.

Шорткат можно создать либо через NSUserActivity, либо через Intent.

Первый случай:

Это старый-добрый Spotlight shortcut, который мы все знаем, но по-умному предлагаемый Siri. Пользователь нажимает на шорткат, который передает команду с параметрами (NSUserActivity) нашему приложению, а оно само решает, как эту команду следует обработать (открыть окно текущего курса USD, или окно заказа нашей любимой пиццы).

Второй случай:

Раньше набор Intent'ов был жестко задан Apple: перевод денег, отправка сообщений, и прочих. Шорткаты, созданные через Intent интереснее — они позволяют выполнить команду сразу в интерфейсе Siri, не запуская вашего приложения. Теперь же, у нас, разработчиков, появилась возможность создавать свои Intent'ы!

Независимо от того, как создавался шорткат — он проходит 3 стадии жизненного цикла:

  1. Объявление (Define)
  2. Доставка в систему (Donate)
  3. Обработка приложением (Handle)

Мое исследование показало, что одно приложение может доставить в систему не более 20 шорткатов.

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

Создание шорткатов через NSUserActivity

Разберем первый, простой тип шорткатов, которые открываются через NSUserActivity.

Для того чтобы попасть на экран с картой банкоматов, мне приходится запустить приложение, перейти на таб "Еще" в таббаре, выбрать раздел "Инфо" и уже там нажать на кнопку "Банкоматы".
Если мы создадим шорткат, который сразу ведет на этот экран — пользователь сможет попасть в него в одно касание, когда Siri предложит ему его, например, на заблокированном экране. Например, в приложение мобильного банка у нас есть экран поиска банкоматов и я часто их ищу.

Объявляем шорткат (Declare)

Первым шагом будет объявление типа нашей NSUserActivity (можно сказать, что это ее идентификатор) в info.playlist:

<key>NSUserActivityTypes</key>
<array> <string>ru.tinkoff.demo.show-cashMachine</string>
</array>

Объявили.

Доставляем шорткат в систему (Donate)

После объявления мы можем создать NSUserActivity в коде нашего приложения с типом, который мы задали выше в info.playlist:

let activity = NSUserActivity(activityType: "ru.tinkoff.demo.show-cashMachine")

Другие свойства не являются необходимыми для добавления в шорткаты, но их присутствие делает шорткат более читаемым и дружелюбным для пользователя. Чтобы активити попала в список шорткатов системы, ей необходимо задать title, и выставить свойство isEligibleForSearch в true.

// Заголовок шортката (Необходим чтобы доставить активити в систему, чтобы создался шорткат)
activity.title = "Найти банкоматы" if #available(iOS 12.0, *) { // Фраза, которая будет предложена пользователю когда он захочет приязать голосовую команду к шорткату activity.suggestedInvocationPhrase = "Покажи банкоматы Тинькофф" // Сири будет обучаться и предлагать шорткат на базе этой активити activity.isEligibleForPrediction = true // (Необходим чтобы доставить активити в систему, чтобы создался шорткат) activity.isEligibleForSearch = true
} // Атрибуты отвечают за настройку отображения шортката
let attributes = CSSearchableItemAttributeSet(itemContentType: "NSUserActivity.searchableItemContentType") /// Иконка для шортката
if let image = UIImage(named: "siriAtmIcon") { attributes.thumbnailData = UIImagePNGRepresentation(image)
} /// Описание шортката
attributes.contentDescription = "Открыть карту с ближайшими банкоматами Тинькофф" /// Выставляем атрибуты на активити
activity.contentAttributeSet = attributes

NSUserActivity есть, чтобы доставить ее в систему, осталось сделать последний шаг. Огонь!

У ViewConroller'а есть свойство userActivity, которому нам надо присвоить созданную выше activity:

self.userActivity = activity

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

Однако у меня это действие не доставило активити в систему и шорткат не появился в списке Примечание: в документации Apple сказано, что вместо присваивания активити вьюконтроллеру, достаточно вызвать у активити метод becomeCurrent().

Alternatively, you can attach the object to a UIViewController or UIResponder object, which also marks the activity as current. Next, call the becomeCurrent() method on the user activity object to mark it as current, which donates the activity to Siri.

Чтобы проверить, что все сработало, открываем Настройки > Siri и поиск — шорткат на базе нашей активити должен быть в списке.

Обработка шортката приложением (Handle)

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

activity пробросится нам в методе AppDelegate'a:

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool return false
}

Итого

Шорткат на базе NSUserActivity создается следующим образом:

  1. Объявляем тип(идентификатор) NSUserActivity в info.plist.
  2. Создаем NSUserActivity в коде и настраиваем
  3. Назначаем активити viewController'у.

Создание голосовых команд из приложения

Нажав на "+", пользователь сможет создать любую голосовую команду и связать ее с выбранным шорткатом. Итак, если пользователь откроет Настройки > Siri и поиск, то увидит список своих шорткатов, который создавался различиными приложениями, и нашим в том числе. Однако каждый раз заходить в настройки неудобно для пользователя, многие даже не догадываются о такой возможности.

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

Мы можем добавить кнопку "добавить действие в Siri"(назвать и нарисовать кнопку можете как угодно) на экран нашего приложения, тогда пользователь, нажав на нее, сможет связать это действие с голосовой командой изнутри приложения, не заходя в настройки. Допустим, пользователь совершил какое-то действие, оно доставилось в систему, он хочет его сохранить.

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

@IBAction func addToSiriAction() { // 1. Смотрим на шорткаты, которые пользователь уже сохранил с фразами INVoiceShortcutCenter.shared.getAllVoiceShortcuts { (shortcuts, error) in guard error == nil, let shortcuts = shortcuts else { // TODO: Handle error return } // 2. Среди этих шорткатов ищем тот, который мы собираемся добавить сейчас let donatedShortcut: INVoiceShortcut? = shortcuts.first(where: { (shorcut) -> Bool in return shorcut.__shortcut.userActivity?.activityType == "com.ba" }) if let shortcut = donatedShortcut { // 3. Если такой шорткат найден - открываем системный экран редактирования шортката. // На нем пользователь сможет обновить фразу голосовой команды let editVoiceShortcutViewController = INUIEditVoiceShortcutViewController(voiceShortcut: shortcut) editVoiceShortcutViewController.delegate = self self.present(editVoiceShortcutViewController, animated: true, completion: nil) } else { // 4. Открываем системный экран добавления голосовой фразы к шорткату let shortcut = INShortcut(userActivity: self.userActivity!) let addVoiceShortcutViewController = INUIAddVoiceShortcutViewController(shortcut: shortcut) addVoiceShortcutViewController.delegate = self } } }

Так, выглядят экраны добавления и редактирования голосовой команды для шортката Siri:

Например, если раньше на экране была кнопка "добавить голосовую команду", то после добавления голосовой команды эта кнопка должна либо исчезнуть либо измениться на "редактировать голосовую команду". Также мы должны реализовать делегатные методы этих viewController'ов, в которых их нужно спрятать dismiss(animated: true, completion: nil) и, если необходимо, обновить текущий экран.

Шорткаты, созданные при помощи Intent

До сих пор мы говорили только про шорткаты, которые открывают приложение, и передают туда опредленные данные данные в NSUserActivity.

Тут начинается самое интересное. Но вернетмся шорткатам, созданным через Intent, которые позволяют совершить некоторые действия не открывая приложение.

Он будет заказывать ее много раз, когда захочет, и он даже добавил голосовую команду на шорткат этой пиццы — и это упрощает его жизнь. Представим, пользователь заказывает свою любимую пиццу. Это как раз тот случай, когда пользователю не понадобится открывать само приложение, чтоб> ы совершить какое-то действие. Но мы можем сделать для него больше — мы можем сделать так, чтобы подав голосовую команду Siri, система не кидала его в приложение, а показала информацию о заказе и заказала пиццу сразу в интерфейсе Siri!

Сначала заходим в настройки проекта, выбираем главный таргет, вкладку Capabilities и включаем доступ к Siri.

Наше приложение может взаимодействовать c Siri, но происходит это не в основном коде приложения, а в отдельном таргете-расширении Intents Extensions

XCode предложит создать еще target-расширение для окошка отображения вашего действия в Siri, если есть в этом потребность, то соглашаемся. Для начала этот таргет необходимо создать: File → New → Target, выбираем Intents Extensions.

Объявляем шорткат (Declare)

Основное нововведение SiriKit'a в iOS 12 — возможность создавать свои Inetnts, к тем, которые уже были раньше.

Для этого нужно создать новый файл: File → New → File, выбрав из секции Resource тип SiriKit Intent Definition File.

Открываем файл, и там где написано "No Intents" внизу есть иконка "+" — нажимаем на нее. В итоге, появится файл с расширением .intentdefinition, в котором можно создавать свои Intents. В списке появится интент, которому можно добавить параметры. "New Intent". Для количества выберем тип Integer, а для вида пиццы выберем тип Custom, что в коде будет представлено классом INObject. В случае действия с заказом пиццы, в качестве параметров можно добавить количество пицц, и вид пиццы для заказа.

Теперь пару строк фрустрации:

Увы! Пользователь не сможет передавать одной и той же сохраненной голосовой команде разные параметры.

Для чего параметры:

Это не значит, что пользователь может говорить фразы "Покажи курс доллара", "Покажи курс биткойна" и т.д. Допустим, вы создаете сущность "Покажи курс %currency", где currency — это параметр сущности. Но это означает, что если пользователь просмотрел курс доллара — то создался шорткат "Покажи курс USD", потом, когда он просмотри курс биткойна, создается шорткат "Покажи курс BTC" и т.д. Из коробки это не будет работать так. Каждому из шорткатов пользователь сможет задать свою голосовую команду. Иными словами у него может быть несколько шоркатов, которые базируются на одной и той же intent, но с разными параметрами.

Хорошо, создав интент в файле .intentdefinition, XCode автоматически сгенерирует класс для этого интента (прим.: он не отобразится в файлах проекта, но будет доступен для использования) Этот автосгенерированный файл будет только в тех таргетах, которым принадлежит .intentdefinition файл.

После создания интента в файле .intentdefinition мы можем создавать наши интенты в коде.

let intent = OrderPizzaIntent()

Доставляем шорткат в систему (Donate)

Для этого создается INInteraction объект с экземпляром вашего интента, и у этого интеракшна вызывается метод .donate Для того, что бы эта сущность попала в список шорткатов — нужно ее задонатить.

let intent = OrderPizzaIntentf()
// ... Настройка сущности let interaction = INInteraction(intent: intent, response: nil) interaction.donate { (error) in // ... Обработка ошибки и/или успеха
}

После выполнения этого кода шорткат на базе интента доставится в систему и отобразится в Настройках Siri.

Обрабатываем шорткат приложением (Handle)

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

Мы уже создали таргет-расширение для Siri и в нем есть предсозданный класс IntentHandler, у которого есть один единственный метод — `handle(for intent)``

class IntentHandler: INExtension { override func handler(for intent: INIntent) -> Any { guard intent is OrderPizzaIntent else { fatalError("Unhandled intent type: \(intent)") } return OrderPizzaIntentHandler() }
}

Прим.: Если компилятор не видит класс вашего интента, значит вы не добавили файл .intentdefinition таргет-расширение для Siri.

Создадим обработчик для нашей OrderPizzaIntent, и реализуем в нем протокол OrderPizzaIntentHandling, который уже автосгенерирован после создания вашей Intent в .intentdefinition. В этом методе мы определяем тип входящего интента и для каждого типа создаем объект-обработчик, который будет обрабатывать этот интент.

Сначала вызывается confirm где происходит проверка всех данных и проверяется доступность выполнения действия. Протокол содержит два метода confirm и handle. Затем сработает handle в коротом действие надо выполнить.

public class OrderPizzaIntentHandler: NSObject, OrderPizzaIntentHandling { public func confirm(intent: OrderPizzaIntent, completion: @escaping (OrderPizzaIntentResponse) -> Void) { // Различные проверки на доступность действия // ... completion(OrderPizzaIntentResponse(code: OrderPizzaIntentResponseCode.ready, userActivity: nil)) } public func handle(intent: OrderPizzaIntent, completion: @escaping (OrderPizzaIntentResponse) -> Void) { // Код с выполнением действия // ... completion(OrderPizzaIntentResponse(code: OrderPizzaIntentResponseCode.success, userActivity: nil)) }
}

Оба этих метода обязательно должны вызвать completion c ответом OrderPizzaIntentResponse(он тоже автосгенерирован), иначе Siri просто будет долго ждать после чего выдаст ошибку.

Более подробные ответы от Siri

Например на этапе confirm может возникнуть несколько разных ошибок — кончилась пицца, пиццерия не работает в это время и т.д. Есть стандартный, автосгенерированный набор кодов ответа — enum OrderPizzaIntentResponseCode, но для дружелюбного интерфейса их может оказаться недостаточно. Помните мы создали Intent в файле .intentdefinition? и пользователю следует узнать о данных фактах, вместо стандартного сообщения "Ошибка в приложении". Вместе с самим интентом создался еще и его Response в котором можно добавить свои варианты ошибок и успешных ответов, и настроить их параметрами:

Теперь мы можем сообщать пользователю более информативные ошибки и ответы:

public func confirm(intent: OrderPizzaIntent, completion: @escaping (OrderPizzaIntentResponse) -> Void) { guard let pizzaKindId = intent.kind?.identifier else { // Если вдруг мы неверно настроили интент для шортката - показываем ошибку по умолчанию completion(OrderPizzaIntentResponse(code: .failure, userActivity: nil)) return } if pizzeriaManager.isPizzeriaClosed == true { /// Если пиццерия уже закрыта - говорим об этом пользователю completion(OrderPizzaIntentResponse(code: .failurePizzeriaClosed, userActivity: nil)) return } else if pizzeriaManager.menu.isPizzaUnavailable(identifier: pizzaKindId) { /// Если пиццы нет в наличии - говорим об этом пользователю completion(OrderPizzaIntentResponse(code: .failurePizzaUnavailable(kind: intent.kind), userActivity: nil)) return } // Раз не возникло ошибок - можем подтвердить действие completion(OrderPizzaIntentResponse(code: .ready, userActivity: nil))
}

Отрисовка Intent

У нас есть MainInterface.storyboard и IntentViewController, в которых мы можем набросать их дизайн. Если мы создали таргет-расширение Intent Extension UI то мы можем нарисовать кастомную вьюшку в Siri для нужных нам интентов. Этот вью контроллер реалзиует протокол INUIHostedViewControlling и конфигурация вьюшки происходит в методе configureView

// Prepare your view controller for the interaction to handle. func configureView(for parameters: Set<INParameter>, of interaction: INInteraction, interactiveBehavior: INUIInteractiveBehavior, context: INUIHostedViewContext, completion: @escaping (Bool, Set<INParameter>, CGSize) -> Void) { // Do configuration here, including preparing views and calculating a desired size for presentation. completion(true, parameters, self.desiredSize) } var desiredSize: CGSize { return self.extensionContext!.hostedViewMaximumAllowedSize }

Что бы этот этот метод вызвался, необходимо в info.plist, который относится к таргету-расширению Intents UI, добавить название нашего интента в массив NSExtension->NSExtensionAttributes->IntentsSupported

<key>NSExtension</key> <dict> <key>NSExtensionAttributes</key> <dict> <key>IntentsSupported</key> <array> <string>OrderPizzaIntent</string> </array> </dict>

Ниже скришоты, как выглядит наш интент в Siri, в поиске и на заблокированном экране. В зависимости от дизайна вашей вьюшки в Siri и от interaction.intent'а, который попал в метод, вы вы можете нарисовать эту вьюшку так, как хотите.

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

Итого

Чтобы его создать нужно: Шорткат на базе Intent может отрисовать в интерфейсе сири или в центре уведомлений и выполнить действие не открывая приложение.

  1. Включаем в Capabilities возможность использовать Siri
  2. Создать таргеты-расширения Intents Extensions и Intents Extensions UI
  3. Создаем SiriKit Intent Definition File
  4. Создаем свой Intent в этом файле и прописываем ему параметры.
  5. Создаем IntentHandler в котором реализуем методы confirm и hanlde

Рекомендации

Общий код в таргете-расширении Siri и в основном приложении

Если у вас есть код, который используется и в таргете для Siri и в таргете основного проекта — есть 2 способа решить этот вопрос:

  1. Выделить общие классы добавить их к обоим таргетам. ( View → Utilites → Show File Inspector'e, в секции Target Membership добавить галочки к таргетам, которым нужен доступ к выделенному файлу )
  2. Создать один или несколько таргетов-framework'ов и унести общий код туда.

Также стоит отметить, что у этих фреймворков желательно выставить флаг Allow app extension API only, тогда, разрабатывая фреймворк, компилятор будет ругаться, если вы попытаетесь использовать API недопустимое в разработке расширений (например UIApplication). Предпочтительнее последний способ, потому что эти фреймворки вы потом сможете использовать в других расширениях и проектах.

Общие ресурсы можно шарить между таргетами через App Groups

Отладка

Тестировать шорткаты помогут помочь:

  1. Настройки телефона Настройки → Developer: переключатели Display Recent Shortcuts и Display Donations on Lock Screen:

  1. Для тестирования Intens можно сразу запускать таргет-расширение, задав в XCode'е фразу, с которой откроется Siri. Для этого нужно выбрать схему для таргета-расширение Siri

Нажать на этот таргет, нажать Edit Scheme...

В поле Siri Intent Query ввести фразу, с которой Siri уже запустится, как будто вы уже сказали ее.

Итого

Предлагаю остановиться и резюмировать, что у нас получилось:

  1. Шорткаты можно создать через NSUserActivity, или через INIntent
  2. Шорткаты нужно объявить(declare), сообщить системе(donate), и обработать(handle).
  3. В приложение можно добавить кнопку "Add to Siri", нажав на которую пользователь добавить фразу для действия и в дальнейшем вызывать его голосом.
  4. Можно создавать свои Intents в добавок ко встроенным.
  5. Через шорткаты на базе Intents можно создавать действия, которые будут выполняться через интерфейс Siri (либо на заблокированном экране или в поиске) без потребности открывать само приложение.

В документации Apple есть ссылка на Demo проект, который полезно скачать и ориентироваться на него при разработке.

И я часто ловлю проблемы и баги. Хотелось бы подчеркнуть, что на момент написания статьи это API на стадии beta. Во время работы мне периодически попадались следующие:

  • Голосовые команды, открывающие Intent в Siri, не открываются.
  • Просмотр предложений Siri не работает с экрана блокировки.
  • Проблема с асинхронными операциями в таргетах для Siri.

Ссылки

  1. WWDC 2018, session 211: Introduction to Siri Shortcuts
  2. WWDC 2018, session 214: Building for Voice with Siri Shortcuts
  3. Apple Developer: SiriKit
  4. Apple Developer: INUIHostedViewControlling
  5. Demo проект Soup Chef от Apple
Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть