Я разрабатываю HomeDeck for HomeKit — полноэкранный дашборд для существующего дома Apple Home на iPad и Apple TV. В этой статье разберу не витрину приложения, а инженерную часть: как представить неоднородный граф HomeKit единым снимком, разделить системный слой и UI, синхронизировать пользовательскую конфигурацию и не превратить общий SwiftUI-код в лес платформенных условий.
Проект начинался с версии для Apple TV. После знакомства с первыми сборками энтузиасты умного дома стали просить такой же дашборд для iPad — прежде всего как настенную или настольную сенсорную панель. Так задача выросла из одного tvOS-приложения в продукт для двух платформ с общими данными, но разными моделями взаимодействия.
Исходная задача
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-классов.
Это дало три практических преимущества:
-
UI-тесты не зависят от домашней инфраструктуры разработчика.
-
Скриншоты App Store воспроизводимы.
-
Представления не обязаны разбирать характеристики аксессуара.
Почему снимок, а не прямое наблюдение за каждой характеристикой
При старте HMHomeManager уже содержит последние известные системе значения. Если последовательно перечитать каждую характеристику большого дома до первого рендера, запуск легко станет заметно медленнее — особенно при доступе через домашний центр.
В HomeDeck используется двухступенчатая схема:
-
Сначала строится снимок из доступного объектного графа HomeKit.
-
Поддерживаемые характеристики обновляются в фоне, а изменения приходят через делегаты и поток событий.
Регистрация наблюдений выполняется до фонового обновления, чтобы не потерять изменение между построением первого снимка и явным чтением.
Сам снимок содержит уже нормализованные сущности интерфейса: ScenePreset, DeviceTile, SensorTile, HomeCamera, комнаты и зоны. Например, UI не должен выяснять, относится ли характеристика CurrentTemperature к термостату или отдельному датчику: это решается при построении модели.
Нормализация устройств
Одинаково называющиеся аксессуары могут публиковать разные сервисы и характеристики. Поэтому тип карточки нельзя надёжно определять только по имени устройства или производителю.
Провайдер анализирует фактический сервис и ассоциированные сервисы аксессуара. Из этого выводятся:
-
тип управления;
-
доступные диапазоны и шаг;
-
текущее состояние;
-
возможность изменения цвета, уровня или режима;
-
принадлежность комнате;
-
доступность аксессуара.
Команда отправляется обратно по стабильному идентификатору нормализованной сущности. После записи интерфейс ждёт подтверждённое HomeKit-состояние и не должен бесконечно жить в оптимистической версии реальности.
Отдельная ветка нужна камерам. HomeKit может вернуть профиль камеры, но реальный поток доступен только при наличии соответствующего streamControl и разрешённой конфигурации. Поэтому наличие камеры в доме ещё не гарантирует одинаковое поведение всех моделей.
Общие данные, разные интерфейсы
Попытка использовать полностью одинаковую композицию на 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.
Безопасность на Apple TV — часть архитектуры
tvOS нельзя считать iPadOS без сенсорного экрана. Платформа иначе относится к чувствительным HomeKit-операциям. Прямые элементы управления замками и охранными системами не должны механически переноситься на общий телевизор.
Поэтому набор отображаемых действий зависит не только от характеристик аксессуара, но и от платформы. Это именно правило домена, а не косметическое скрытие кнопки. UI не показывает действие, которое приложение не должно предлагать на данном устройстве.
Есть и ещё одно ограничение: приложение не может подменить системную заставку tvOS. Ambient mode в HomeDeck работает только пока открыто само приложение. Это важно явно объяснять пользователю и учитывать в жизненном цикле.
Синхронизация конфигурации без собственного сервера
Пользователь настраивает состав панели на 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 и не может компенсировать отсутствующие характеристики серверной интеграцией производителя.
Что бы я заложил с первого дня
Если бы я снова начинал подобный проект, я бы сразу зафиксировал пять решений:
-
Собственная нормализованная модель между HomeKit и UI.
-
Протокол провайдера с боевой и демонстрационной реализациями.
-
Быстрый первый снимок и отдельное фоновое обновление значений.
-
Общий слой данных, но отдельная композиция iPadOS и tvOS.
-
Платформенные ограничения безопасности как часть доменной модели.
Главный урок: кроссплатформенный 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.
Автор: Homedeck
