Я разрабатываю HomeDeck for HomeKit — полноэкранный дашборд для существующего дома Apple Home на iPad и Apple TV. В этой статье разберу не витрину приложения, а инженерную часть: как представить неоднородный граф HomeKit единым снимком, разделить системный слой и UI, синхронизировать пользовательскую конфигурацию и не превратить общий SwiftUI-код в лес платформенных условий.

Проект начинался с версии для Apple TV. После знакомства с первыми сборками энтузиасты умного дома стали просить такой же дашборд для iPad — прежде всего как настенную или настольную сенсорную панель. Так задача выросла из одного tvOS-приложения в продукт для двух платформ с общими данными, но разными моделями взаимодействия.

HomeDeck for HomeKit на Apple TV
HomeDeck for HomeKit на Apple TV

Исходная задача

HomeKit возвращает не готовые карточки интерфейса, а объектный граф: дома, комнаты, аксессуары, сервисы и характеристики. При этом фактические возможности зависят от конкретного устройства.

Нужно было собрать из этого графа экран, на котором одновременно живут:

  • комнаты и зоны;

  • сценарии;

  • свет, розетки, шторы, климат и другие устройства;

  • датчики температуры, влажности, движения, контакта и дыма;

  • камеры;

  • состояние безопасности;

  • события и команды управления.

Вторая часть задачи — две заметно разные платформы. iPad предполагает touch, изменение порядка и детальную настройку. Apple TV — направленный focus, пульт и чтение с нескольких метров. Данные общие, а модель взаимодействия разная.

Для tvOS была и отдельная продуктовая мотивация: на Apple TV нет полноценного системного приложения «Дом» уровня iPhone и iPad, а штатный интерфейс предоставляет лишь отдельные элементы управления. Поэтому сторонний дашборд здесь решает не только вопрос компоновки, но и даёт постоянное полноэкранное представление дома.

Слой между HomeKit и SwiftUI

Связывать представления напрямую с HMAccessory, HMService и HMCharacteristic оказалось неудобно. Такой UI быстро начинает знать слишком много о HomeKit, а превью, тесты и демонстрационный режим требуют живого дома.

Поэтому системный слой закрыт протоколом провайдера:

@MainActor
protocol HomeDashboardProviding {
    func loadSnapshot() async throws -> DashboardSnapshot
    func eventStream() -> AsyncStream<HomeEvent>
    func configurationChangeStream() -> AsyncStream<Void>

    func performScene(id: ScenePreset.ID) async throws
    func setDevicePower(id: DeviceTile.ID, isOn: Bool) async throws
    func setDeviceLevel(id: DeviceTile.ID, level: Double) async throws
    func setCurtainLevel(id: DeviceTile.ID, level: Double) async throws
    func setThermostatTarget(id: DeviceTile.ID, target: Double) async throws
}

Боевой HomeKitDashboardProvider реализует протокол через HMHomeManager, а mock-провайдер возвращает детерминированный дом. SwiftUI получает DashboardSnapshot — собственную модель представления, не зависящую от HomeKit-классов.

Это дало три практических преимущества:

  1. UI-тесты не зависят от домашней инфраструктуры разработчика.

  2. Скриншоты App Store воспроизводимы.

  3. Представления не обязаны разбирать характеристики аксессуара.

Почему снимок, а не прямое наблюдение за каждой характеристикой

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

В HomeDeck используется двухступенчатая схема:

  1. Сначала строится снимок из доступного объектного графа HomeKit.

  2. Поддерживаемые характеристики обновляются в фоне, а изменения приходят через делегаты и поток событий.

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

Сам снимок содержит уже нормализованные сущности интерфейса: ScenePreset, DeviceTile, SensorTile, HomeCamera, комнаты и зоны. Например, UI не должен выяснять, относится ли характеристика CurrentTemperature к термостату или отдельному датчику: это решается при построении модели.

Нормализация устройств

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

Провайдер анализирует фактический сервис и ассоциированные сервисы аксессуара. Из этого выводятся:

  • тип управления;

  • доступные диапазоны и шаг;

  • текущее состояние;

  • возможность изменения цвета, уровня или режима;

  • принадлежность комнате;

  • доступность аксессуара.

Команда отправляется обратно по стабильному идентификатору нормализованной сущности. После записи интерфейс ждёт подтверждённое HomeKit-состояние и не должен бесконечно жить в оптимистической версии реальности.

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

Главный экран HomeDeck for HomeKit
Главный экран HomeDeck for HomeKit

Общие данные, разные интерфейсы

Попытка использовать полностью одинаковую композицию на iPad и Apple TV быстро ломается.

Для iPad важны:

  • touch targets и жесты;

  • drag-and-drop для порядка элементов;

  • изменение размера окна и Split View;

  • Dynamic Type;

  • pointer и клавиатура как дополнительные способы ввода.

Для tvOS важны:

  • предсказуемый focus graph;

  • заметные состояния focused/pressed/selected;

  • достаточные интервалы для focus scale;

  • читаемость с дивана;

  • логика Back и Siri Remote.

Поэтому HomeDeck делит не данные, а платформенную композицию. Общие модели, хранилище и большинство компонентов остаются едиными, а верхний layout выбирается отдельно:

#if os(tvOS)
DashboardAppleTVLayout(...)
#else
DashboardIPadLayout(...)
#endif

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

HomeDeck for HomeKit на настенном iPad
HomeDeck for HomeKit на настенном iPad

Безопасность на Apple TV — часть архитектуры

tvOS нельзя считать iPadOS без сенсорного экрана. Платформа иначе относится к чувствительным HomeKit-операциям. Прямые элементы управления замками и охранными системами не должны механически переноситься на общий телевизор.

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

Есть и ещё одно ограничение: приложение не может подменить системную заставку tvOS. Ambient mode в HomeDeck работает только пока открыто само приложение. Это важно явно объяснять пользователю и учитывать в жизненном цикле.

Ambient mode на Apple TV
Ambient mode на Apple TV

Синхронизация конфигурации без собственного сервера

Пользователь настраивает состав панели на iPad: выбирает избранное, скрывает лишние элементы и меняет порядок. На Apple TV должна появиться та же конфигурация.

Для небольшого объёма настроек хватает NSUbiquitousKeyValueStore. Данные сохраняются отдельно для каждого HomeKit-дома, а локальный UserDefaults используется как кэш:

func save(_ references: [FavoriteReference], homeID: String) {
    let key = storageKey(homeID: homeID)
    let values = references.map(\.storageValue)
    localStore.set(values, forKey: key)
    cloudStore?.set(values, forKey: key)
    cloudStore?.synchronize()
}

При этом важно установить обработчик внешних изменений до первого synchronize(). Иначе на свежей установке Apple TV можно пропустить первоначальное уведомление синхронизации.

Хранить весь снимок дома в iCloud не требуется. Синхронизируются только идентификаторы и пользовательский порядок, а актуальные названия и состояния снова берутся из HomeKit на конкретном устройстве.

Privacy by architecture

В проекте нет отдельного аккаунта HomeDeck и собственного сервера с копией дома. HomeKit-объекты и совместимые видеопотоки обрабатываются на устройстве через системные фреймворки Apple. Настройки проходят через iCloud пользователя, погода — через WeatherKit, покупки — через StoreKit.

Это не только маркетинговое обещание, но и уменьшение количества состояний, которые пришлось бы синхронизировать и защищать. Обратная сторона — приложение принимает ограничения Apple API и не может компенсировать отсутствующие характеристики серверной интеграцией производителя.

Что бы я заложил с первого дня

Если бы я снова начинал подобный проект, я бы сразу зафиксировал пять решений:

  1. Собственная нормализованная модель между HomeKit и UI.

  2. Протокол провайдера с боевой и демонстрационной реализациями.

  3. Быстрый первый снимок и отдельное фоновое обновление значений.

  4. Общий слой данных, но отдельная композиция iPadOS и tvOS.

  5. Платформенные ограничения безопасности как часть доменной модели.

Главный урок: кроссплатформенный SwiftUI экономит код только тогда, когда общий слой выбран правильно. В HomeDeck общими оказались данные и базовые компоненты. Навигация, плотность экрана и модель взаимодействия остались платформенными — и именно это позволило не превратить телевизионный интерфейс в растянутый iPad.

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

HomeDeck for HomeKit уже доступен в App Store. Подробнее о проекте — на сайте: https://www.homedeck.co/ru. Присоединиться к тестированию новых сборок можно через TestFlight: https://testflight.apple.com/join/V1GdMNgw. Обсудить приложение, поделиться идеями и пообщаться с другими энтузиастами умного дома можно в Telegram-группе: https://t.me/+d3TdcBnvujphM2Y6.

Если тема интересна, в следующем материале могу подробнее разобрать преобразование HomeKit-сервисов в типизированные карточки или focus-навигацию tvOS.

Комментарии (0)