Connect with us

Разработка

Учим Siri говорить о вашем любимом приложении: пример Walk Mate

Spotlight уже знает, что прогулка существует. Теперь Siri в iOS 27 должна уметь говорить о ней.

Опубликовано

/

     
     

В посте на прошлой неделе я рассказал, как Walk Mate начал передавать избранные маршруты в Spotlight. Добавляете маршрут в избранное и задаёте ему название. Ищете в Spotlight, нажимаете — маршрут открывается на карте. Для этого использовались App Intents без особых изысков: IndexedEntity для избранного маршрута, OpenIntent для его загрузки и небольшой маршрутизатор, чтобы нажатие в Spotlight попадало в тот же редьюсер, что и нажатие внутри приложения. Фраз для Siri тогда ещё не было.

Spotlight уже знает, что прогулка существует. Теперь Siri в iOS 27 должна уметь говорить о ней.

В прошлый раз Spotlight мог находить избранный маршрут. Теперь я делаю так, чтобы Siri могла говорить о нём, передавать его в Карты и открывать голосом.

1. Дайте сущности «голос»: @Property и ключевые слова Spotlight

Прежде чем Siri сможет ссылаться на избранный маршрут, сущности, которая за ним стоит, нужно поле, которое Siri разрешено произносить вслух. Я обновил WalkMatePlaceEntity так, чтобы name было обёрнуто в @Property. Эта обёртка публикует поле, чтобы Shortcuts могли его отображать, а Siri — произносить. В Xcode 16+ заголовки для @Property и @Parameter необязательны. Apple автоматически создаёт локализуемый заголовок из имени свойства, поэтому я указываю title: только тогда, когда подпись в Shortcuts должна отличаться от имени ключа.

struct WalkMatePlaceEntity: IndexedEntity {
    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Favorite walk"
    static let defaultQuery = WalkMatePlaceEntityQuery()
    static let spotlightIndexName = "WalkMateFavorites"

    let id: UUID
    @Property
    var name: String?
    var startLatitude: Double
    var startLongitude: Double
}

Я также сохраняю начальные координаты маршрута прямо в entity — они понадобятся позже для передачи в Карты. Обратите внимание: сущность экспортирует только начальную точку маршрута, а не всю прогулку.

Поиск Spotlight по-прежнему работает так же, как было показано в части 1: поиск по «Berlin» находит избранный маршрут с названием Berlin, потому что attributeSet добавляет имя не только в заголовок, но и в ключевые слова Spotlight:

var attributeSet: CSSearchableItemAttributeSet {
    let attributes = defaultAttributeSet
    attributes.displayName = name
    attributes.title = name
    if let name {
        attributes.keywords = [name]
    }
    return attributes
}

2. Разрешите Shortcuts искать и переиндексировать

EnumerableEntityQuery не изменился. Spotlight и Shortcuts по-прежнему находят избранный маршрут по идентификатору через entities(for:) и получают весь список через allEntities().

Сверху добавились две вещи:

1) Когда параметр в Shortcuts означает «выбрать сохранённую прогулку», suggestedEntities() предлагает полный список, а EntityStringQuery фильтрует его по мере ввода:

func suggestedEntities() async throws -> [WalkMatePlaceEntity] {
    try await allEntities()
}

extension WalkMatePlaceEntityQuery: EntityStringQuery {
    func entities(matching string: String) async throws -> [WalkMatePlaceEntity] {
        try await allEntities().filter { entity in
            entity.name?.localizedStandardContains(string) == true
        }
    }
}

2) iOS 27 может попросить приложение перестроить индекс Spotlight. IndexedEntity описывает структуру данных, которые попадают в индекс, но сам по себе переиндексацию не выполняет. IndexedEntityQuery — это интерфейс, который Spotlight вызывает, когда индекс устарел:

@available(iOS 27, *)
extension WalkMatePlaceEntityQuery: IndexedEntityQuery {
    func reindexEntities(
        for identifiers: [WalkMatePlaceEntity.ID],
        indexDescription: CSSearchableIndexDescription
    ) async throws {
        let entities = FavoriteRouteStorage.load()
            .filter { identifiers.contains($0.id) }
            .compactMap(WalkMatePlaceEntity.init)
        try await CSSearchableIndex.default().indexAppEntities(entities)
    }

    func reindexAllEntities(
        indexDescription: CSSearchableIndexDescription
    ) async throws {
        let entities = FavoriteRouteStorage.load().compactMap(WalkMatePlaceEntity.init)
        try await CSSearchableIndex.default().indexAppEntities(entities)
    }
}

Используется тот же FavoriteRouteStorage.load(). Система передаёт CSSearchableIndexDescription; я записываю данные в тот же индекс по умолчанию, который использовался в части 1.

3. Держите список фраз Siri в актуальном состоянии

donateAll() из части 1 удалял все WalkMatePlaceEntity из Spotlight, а затем заново индексировал то, что всё ещё находилось в SQLite. Эта запись не изменилась. Новое здесь то, что теперь функция также сообщает App Shortcuts, что названия избранных маршрутов изменились, поэтому фраза вроде «Load Berlin in Walk Mate» остаётся актуальной после переименования, добавления или удаления маршрута.

static func donateAll() {
    Task { @MainActor in
        WalkMateAppShortcuts.updateAppShortcutParameters()
    }
    Task {
        guard CSSearchableIndex.isIndexingAvailable() else { return }
        let entities = FavoriteRouteStorage.load().compactMap(WalkMatePlaceEntity.init)
        let namedIndex = CSSearchableIndex(name: spotlightIndexName)
        try? await namedIndex.deleteAppEntities(ofType: WalkMatePlaceEntity.self)
        let index = CSSearchableIndex.default()
        try? await index.deleteAppEntities(ofType: WalkMatePlaceEntity.self)
        guard !entities.isEmpty else { return }
        try? await index.indexAppEntities(entities)
    }
}

Функция App.init также один раз вызывает updateAppShortcutParameters() при запуске, поэтому кэш фраз уже прогрет к тому моменту, когда Siri впервые понадобится его использовать.

4. Открывайте избранный маршрут голосом

В части 1 был один OpenWalkMatePlaceIntent: OpenIntent. Он по-прежнему используется для открытия в iOS 27, но теперь также принимает системную схему Apple system.open, благодаря которой Siri AI воспринимает «открой этот избранный маршрут» как системное действие открытия. TargetContentProvidingIntent — это часть iOS, которая фактически переводит сцену к нужной сущности.

@available(iOS 27, *)
@AppIntent(schema: .system.open)
struct OpenWalkMatePlaceIntent: OpenIntent, TargetContentProvidingIntent {
    var target: WalkMatePlaceEntity

    @MainActor
    func perform() async throws -> some IntentResult {
        QuickActionRouter.dispatch(.openFavorite(id: target.id))
        return .result()
    }
}

ShowSavedWalkIntent — пользовательский интент, который работает на iOS 18+: тот же диспатчер, тот же редьюсер.

struct ShowSavedWalkIntent: AppIntent {
    static let title: LocalizedStringResource = “Show Saved Walk”
    static let openAppWhenRun = true

    @Parameter(title: “Saved walk”)
    var walk: WalkMatePlaceEntity

    @MainActor
    func perform() async throws -> some IntentResult {
        QuickActionRouter.dispatch(.openFavorite(id: walk.id))
        return .result()
    }
}

Оба отправляют .openFavorite(id:). В части 1 этот идентификатор уже ставился в очередь во время заставки. Нажимаете “Berlin” в Spotlight, Shortcuts или Siri — и получаете ту же карту, что и при нажатии “Berlin” внутри Favorite Routes.

5. Дайте Siri фразу, которую можно произнести

Одного AppIntent недостаточно, чтобы Siri начала воспринимать его как команду. Нужно зарегистрировать его через AppShortcutsProvider. Каждая фраза должна содержать \(.applicationName).

struct WalkMateAppShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: ShowSavedWalkIntent(),
            phrases: [
                “Load \(\.$walk) in \(.applicationName)”
            ],
            shortTitle: “Open Favorite Walk”,
            systemImageName: “figure.walk”
        )
    }
}

\(\.$walk) разворачивается в названия переданных избранных маршрутов. Именно поэтому donateAll() и App.init вызывают updateAppShortcutParameters(). После того как вы назвали прогулку “Berlin”, фраза “Load Berlin in Walk Mate” становится настоящей командой.

(Часть 1 уже показывала «Open Favorite Walk» и «Find Favorite walk» внутри приложения Shortcuts, потому что система отображает все доступные интенты. Плитка и голосовая фраза — новые. Они появляются благодаря этому провайдеру.)

Учим Siri говорить о вашем любимом приложении: пример Walk Mate

6. Понимайте «эта прогулка» на экране

Siri может понимать под «этой прогулкой» строку, на которую вы сейчас смотрите, если представление помечено соответствующим образом. Каждая FavoriteRouteCard получает идентификатор сущности через appEntityIdentifier в iOS 18.4+.

func walkMateEntityIdentifier(_ id: UUID) -> some View {
    if #available(iOS 18.4, *) {
        appEntityIdentifier(
            EntityIdentifier(for: WalkMatePlaceEntity.self, identifier: id)
        )
    } else {
        self
    }
}

Это не связано с экспортом данных. Такая разметка просто позволяет сказать «третья прогулка в списке избранного», пока список открыт, и Siri поймёт, о какой именно записи идёт речь.

7. Передавайте начальную точку прогулки в Apple Maps

Избранный маршрут — это приватный цикл. Схема места в Apple Maps (@AppEntity(schema: .maps.place)) предназначена для точки интереса, у которой есть часы работы, категория и прочие свойства. Я не стал использовать .maps.place, потому что это не соответствует тому, что предоставляет Walk Mate.

Вместо этого я экспортирую PlaceDescriptor для первой координаты и названия. Shortcuts или Siri могут передать его в Карты, чтобы пользователь мог добраться до точки, где начинается прогулка. Карты не умеют принимать полилинию Walk Mate. Дополнительные промежуточные точки привели бы к тому, что Карты построили бы уже свой маршрут.

@available(iOS 26.4, *)
extension WalkMatePlaceEntity: Transferable {
    static var transferRepresentation: some TransferRepresentation {
        ValueRepresentation(exporting: \.startPlace)
    }
}

@available(iOS 26, *)
extension WalkMatePlaceEntity {
    var startPlace: PlaceDescriptor {
        PlaceDescriptor(
            representations: [
                .coordinate(
                    CLLocationCoordinate2D(
                        latitude: startLatitude,
                        longitude: startLongitude
                    )
                )
            ],
            commonName: nil
        )
    }
}

@available(iOS 26.4, *) относится не к самому Transferable и не к iOS 27. ValueRepresentation(IntentValueRepresentation) — это API, которое экспортирует AppEntity как структурированное системное значение (PlaceDescriptor), а не как файл. Представления файлов или данных не могут переносить координату, а Картам нужно именно значение места. Apple добавила такой структурированный экспорт в iOS 26.4.

Учим Siri говорить о вашем любимом приложении: пример Walk Mate

8. Синхронизируйте все устройства: SyncableEntity

Избранные маршруты в Walk Mate уже синхронизируются между устройствами через CloudKit, используя один и тот же UUID (favorite.id) везде. Протокол SyncableEntity в iOS 27 сообщает Siri, что этот идентификатор стабилен на всех устройствах пользователя, поэтому разговор об избранном маршруте может продолжиться, например, с iPhone на iPad.

Apple пишет о SyncableEntity:

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

Поскольку WalkMatePlaceEntity.id уже представлял собой UUID, синхронизируемый через CloudKit, подключение протокола свелось к одной строке:

@available(iOS 27, *)
extension WalkMatePlaceEntity: SyncableEntity {}

9. Передавайте системе действие, а не только сущность

В части 1 сущности передавались в Spotlight. Так система узнаёт, что прогулка существует. Но она не узнаёт, что вы её открываете.

Когда избранный маршрут открывается из интерфейса Favorites, после успешного открытия я передаю системе и это действие. В iOS 27 передаваемый интент — системное открытие.

donateFavoriteOpened: { favorite in
    guard let entity = WalkMatePlaceEntity(favorite) else { return }
    if #available(iOS 27, *) {
        var intent = OpenWalkMatePlaceIntent()
        intent.target = entity
        IntentDonationManager.shared.donate(intent: intent)
    } else {
        var intent = ShowSavedWalkIntent()
        intent.walk = entity
        IntentDonationManager.shared.donate(intent: intent)
    }
}

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

Фразы (голосовые команды Siri для сохранённых прогулок и поиска)

🗣️ “Generate a route in Walk Mate”, “Generate me a route in Walk Mate”, “Make me a walk in Walk Mate”, “Create a walk in Walk Mate”, “Generate walking routes in Walk Mate”

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

🗣️ “Load Riverside Loop in Walk Mate”

Siri открывает Walk Mate сразу на маршруте Riverside Loop на карте.

🗣️ “Open Riverside Loop in Walk Mate” или “Open Riverside Loop with Walk Mate”

Siri определяет нужную сущность и открывает Riverside Loop на карте.

🗣️ “Search Riverside Loop in Walk Mate”

Приложение открывается на переднем плане, ищет в избранном и показывает Riverside Loop на карте.

🗣️ “Search Central Park in Walk Mate” (это не избранный маршрут)

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

Приложение Shortcuts

Генерация маршрутов с произвольной дистанцией

1. Откройте приложение Команды.
2. Нажмите + (Новая команда) → Добавить действие.
3. Найдите Walk Mate → выберите Generate Routes.
4. Установите Distance в 5 km.
5. Нажмите Run.

Walk Mate открывается на переднем плане и начинает генерировать маршрут длиной примерно 5 км. Кроме того, Walk Mate может открываться и генерировать маршруты с текущей целевой дистанцией по умолчанию без указания Distance в приложении «Команды». Есть также ограничение по верхней и нижней границе: если пользователь вводит дистанцию больше 20 км или меньше 3 км, значение безопасно ограничивается рабочим диапазоном 3–20 км.

Учим Siri говорить о вашем любимом приложении: пример Walk Mate

Когда запускается команда Open Favorite Route, она спрашивает, с каким избранным маршрутом нужно открыть приложение:

Учим Siri говорить о вашем любимом приложении: пример Walk Mate

Если у вас несколько прогулок с похожими названиями — например, «Park North» и «Park South» — и вы говорите “Open Park in Walk Mate”, Siri не может однозначно определить одну из них и поэтому спрашивает “Which one?”, показывая список совпадающих маршрутов.

Что я не добавлял

Хотя в Walk Mate нет строки поиска или отдельного экрана результатов поиска, .system.searchInApp остаётся, и Siri передаёт Walk Mate поисковый запрос. Если найдено одно точное совпадение, приложение загружает этот избранный маршрут, иначе открывается существующий список Favorites. Поскольку в Walk Mate нет кнопки запуска или остановки прогулки, Siri тоже нечего нажимать для начала сеанса ходьбы. Вы просто идёте — а медали приходят сами. Возможности передать весь кольцевой маршрут в Apple Maps тоже нет.

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: