Я разрабатываю 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.