Xcode 26.3 стал первой версией, в которой Apple открыла собственные фичи внешним агентам через MCP. Многие из этих возможностей и раньше были доступны через CLI, но рендеринг Preview был совершенно новым. В Xcode 27 Apple пошла ещё дальше и добавила headless MCP-сервер, сделав эти инструменты ещё удобнее для вызова, поэтому я глубоко встроил рендеринг Preview в свои сценарии ИИ-разработки. По пути я столкнулся с несколькими проблемами, а также сформулировал несколько наблюдений и надежд.
Preview не умеет использовать нужное устройство для рендеринга
В Xcode устройство, которое используется для Preview, не обязательно совпадает с выбранным симулятором или физическим устройством — разработчик может отдельно выбрать устройство именно для Preview.
Возьмём мой MacBook. Поскольку у меня установлены и Xcode 27.0, и Xcode 27.1 beta, даже в версии 27.0 устройством Preview по умолчанию становится iPhone Duo. Если вручную переключиться на другое устройство или платформу, Preview последует за выбором — но только в обычном интерактивном интерфейсе Xcode.
Стоит перейти к инструменту рендеринга, который предоставляет Xcode MCP, и обнаруживается, что указать устройство для рендеринга вообще невозможно — даже через встроенный ИИ-чат Xcode.
Причина в том, что у MCP-инструмента рендеринга сейчас нет параметра для выбора целевого устройства, а значению renderedDestination в результате тоже нельзя доверять — оно показывает симулятор, на котором физически выполняется рендеринг, а не модель устройства, которую Preview фактически эмулирует.
Макрос #Preview тоже не содержит параметра для выбора устройства и молча игнорирует .previewDevice.
Поэтому сейчас единственный способ, которым мне удаётся гарантированно рендерить Preview на конкретном устройстве и получать корректный результат, — вернуться к устаревшему PreviewProvider:
@available(*, deprecated, message: "Capture preview: only PreviewProvider can specify a device")
struct CapturePreview_home: PreviewProvider {
static var previews: some View {
HomeScreens.view(for: "home")
.previewDevice(PreviewDevice(rawValue: "iPhone 18 Pro"))
.previewDisplayName("home")
}
}
Компромисс в том, что отказ от макроса #Preview означает отказ и от многих новых возможностей Preview. Если Preview нужен вам для CI/CD или ИИ-сценариев, лучше держать отдельный набор Preview-кода специально для таких случаев.
Код представления изменился, а скриншот — нет
Ещё более раздражающая проблема в автоматизированных сценариях: код представления явно изменился, а инструмент рендеринга Preview всё равно возвращает старое изображение — и при этом не сообщает ни об одной ошибке.
Часто Preview-код и код самого представления находятся в разных файлах. По моим наблюдениям, если изменить только другие файлы, от которых зависит файл с Preview, Xcode иногда пропускает пересборку и просто возвращает предыдущий результат. Обновление времени модификации файла через touch не помогает; надёжно запускает пересборку только реальное изменение содержимого самого файла Preview.
Особенно заметна проблема при рендеринге представлений напрямую из SPM-пакета — в моих тестах это происходило в 20–40% случаев. В Xcode-проекте (.xcodeproj) частота была значительно ниже — в моих тестах проблема больше не повторилась, хотя это ещё не означает, что её там вообще не бывает.
Конечно, агент может сравнивать хэш скриншота или даже смотреть на длительность рендеринга, чтобы попытаться определить, действительно ли изображение новое. Но это лишь косвенные признаки — они не гарантируют, что возвращённый скриншот действительно соответствует тому состоянию, которое агент запросил.
К счастью, каждый раз, когда я делаю скриншот, я одновременно собираю и дополнительную информацию из представления — id, instance, label каждого .designAnchor, а также его frame в координатах окна. Поэтому я просто добавил туда ещё и fingerprint. Процесс выглядит так:
- Инструмент вычисляет fingerprint по содержимому исходников.
- Он изменяет Preview-код и встраивает туда fingerprint. Сам факт изменения содержимого Preview-файла уже заставляет Xcode пересобраться.
- Затем он получает скриншот вместе с соответствующей информацией в JSON.
- Он сравнивает fingerprint из JSON с тем, который вычислил сам. Если они не совпадают, Preview-файл изменяется ещё раз и выполняется повторный рендеринг. Если и после этого fingerprint не совпал, скриншот отбрасывается.
fileprivate enum SourceFingerprint_8848880ac81b6df2 {}
struct CapturePreview_home: PreviewProvider {
static var previews: some View {
HomeScreens.view(for: "home")
/// Embed the fingerprint
.designProbe("home", build: String(describing: SourceFingerprint_8848880ac81b6df2.self))
.previewDevice(PreviewDevice(rawValue: "iPhone 18 Pro"))
}
}
Обратите внимание: fingerprint — это имя типа, а не строковый литерал. Preview умеет выполнять горячую замену литералов, поэтому изменение только литерала может вообще не вызвать пересборку. В результате возникает иллюзия «нового fingerprint поверх старого кода». Имя типа, напротив, меняется только после реальной перекомпиляции.
Код .designProbe можно посмотреть здесь.
Благодаря такому подходу каждый полученный мной скриншот гарантированно соответствует текущему коду, и устаревшее изображение больше не может случайно просочиться в процесс. Дополнительно информация об anchor позволяет давать гораздо более точечную обратную связь в моих ИИ-сценариях.
Другие проблемы, с которыми я столкнулся
При использовании инструмента рендеринга Preview регулярно встречаются и другие нюансы:
- Процесс Preview-host падает после изменений кода. Стек падения заканчивается внутри
SwiftUICoreнаDebugReplaceableViewChild.updateValue()из-за неудачного приведения типа во время горячей замены реализации представления в Preview. Частота сильно зависит от того, переписывался ли Preview-файл — в худшем случае падение происходит почти в половине запусков. К счастью, немедленный повтор обычно решает проблему. - Однострочные
#Previewсбивают индекс. Если три#Previewв одном файле записаны каждый в одну строку,previewDefinitionIndexInFile: 2рендерит первый Preview с индексом 0, хотя возвращаемыйsourceLineNumberостаётся правильным. Если записать их в несколько строк, всё работает идеально. Вывод простой: всегда пишите#Previewв несколько строк. - После добавления или удаления файлов они могут не находиться. Рендеринг сразу после создания нового файла может вернуть
FileNotFoundError; а рендеринг неизменённого файла сразу после удаления другого —ProviderError: noPreviewInfos. Иногда достаточно подождать секунду-другую, но headless-режим кэширует структуру проекта, поэтому новые файлы часто не распознаются, пока не закрыть и заново не открыть workspace.
Несмотря на все эти проблемы, инструмент рендеринга Preview в Xcode всё равно даёт разработчикам и создателям огромные удобства и открывает большое пространство для экспериментов.
AI + Preview + SwiftUI == творческий инструмент для Apple-разработчиков
На разных стадиях разработки требования к UI отличаются. Например, на этапе формирования идеи мне нравится собирать и систематизировать мысли в документах — и здесь ИИ уже может очень сильно помогать. Но даже если превратить такие документы в блок-схемы или анализ состояний, результат для многих разработчиков всё ещё остаётся недостаточно наглядным.
Поэтому я встроил рендеринг Preview прямо в этот этап. Интерфейс здесь не обязан быть точным, и не нужно заботиться о runtime-логике, но если встроить соответствующие скриншоты в анализ flow, работать становится намного быстрее. Кроме того, значительно проще замечать проблемы и сценарии, о которых раньше даже не подумал.
В видео ниже показан FlowCanvas — инструмент, который я сделал сам. Он превращает документы в визуальный интерактивный canvas и поддерживает его синхронизированным с документами после каждого изменения.
SwiftUI Preview сам по себе тоже является отличным инструментом для творчества. Если вы вообще не понимаете, куда развивать дизайн интерфейса, можно позволить агенту исследовать разные варианты, а затем итеративно уточнять их через обратную связь и диалог, пока не найдётся нужный вариант отображения.
В видео ниже показан FormStudio — ещё один мой инструмент, построенный поверх рендеринга Preview.
Пока я создавал и использовал эти инструменты, меня постоянно преследовала мысль: насколько здорово было бы, если бы Xcode предоставлял подобные возможности напрямую? Даже просто открыть больше plugin-интерфейсов — независимо от CLI или MCP — чтобы разработчики могли расширять Xcode самостоятельно, уже было бы отличным вариантом.
В эпоху ИИ роль IDE становится менее заметной, но в работе, связанной с взаимодействием и дизайном, для разработчиков-людей всё ещё остаётся значительное пространство.
Во времена UIKit Xcode предлагал Storyboard — отличный инструмент, который пытался объединить дизайн и логику. Но в эпоху SwiftUI и ИИ у нас остался лишь фрагментарный набор Preview, через который можно смотреть на большую картину словно через замочную скважину.
Xcode пора меняться — он не должен служить только тем, кто пишет код. ИИ стремительно стирает границы между программистами, дизайнерами и продакт-менеджерами.
Когда Xcode перестанет быть привязан исключительно к коду, он всё ещё сможет завоевать любовь всех этих людей.

