Макрос #Preview в языке Swift предоставляет удобный способ создания и предварительного просмотра компонентов пользовательского интерфейса. Он позволяет разработчикам быстро и легко создавать превью для своих View, чтобы визуально оценить, как они выглядят и взаимодействуют.

Сейчас доступно много информации о том, как писать макросы, много примеров и на удивление хорошая документация. Сегодня мы будем не создавать свой макрос, а подробно рассмотрим приватные макросы, предоставляемые Apple, а именно #Preview.
Как #Preview работает?
В целом это совершенно обычный макрос, но мы не знаем что у него внутри. Пока давайте добавим превью, используя новый макрос:
#Preview { Text("Simple text") }

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

Для просмотра сгенерированного кода можно использовать Xcode, нажав на Expand Macro:

А для понимания, где генерируемый код располагается, мы можем воспользоваться командой swiftc:
swiftc TextPreviewView.swift
Не мог не отметить, что есть ещё вариант просмотра результата работы макроса с помощью тестов
При запуске команды мы получим сгенерированный файл @__swiftmacro_04TestA4View33_DC5l7PreviewfMf0 по пути /var/folders/zd/gc5dlw40000gq/T/swift-generated-sources/.
Открыв файл, мы увидим следующий код и сможем изучить результаты работы макроса (Xcode 15 Beta 2):
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) struct $s04TestA4View33_DC5l7PreviewfMf0_15PreviewRegistryfMu_: PreviewRegistry { static var fileID: String { "TextPreviewView.swift" } static var line: Int { 17 } static var column: Int { 1 } static var preview: Preview { Preview { Text("Simple text") } } } #if os(xrOS) @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) @objc final class $s04TestA4View33_DC5l7PreviewfMf0_17UVPreviewRegistryfMu_: UVPreviewRegistry { override var fileID: String { "TextPreviewView.swift" } override var line: Int { 17 } override var column: Int { 1 } override var preview: Preview { Preview { Text("Simple text") } } } #endif // original-source-range: TextPreviewView.swift:17:1-19:2
Немного подробнее о сгенерированном коде
Код получился довольно не большим, но давайте добавим разъяснения того, что в нём и зачем.
Обратим внимание на два, практически одинаковых, объекта:
struct $s04TestA4View3... и class $s04TestA4View3…
Интересно что class доступен только для xrOS(VisionOS). Тяжело предположить зачем Apple сделала структуру и класс с одинаковыми параметрами, а не использовала просто структуру. Может быть class с пометкой @objcиспользуется для поиска Preview в runtime, но зачем тогда пракически идентичная структура — вопрос открытый.
Начнем с протокола, которому соответствует struct $s04TestA4View3....
PreviewRegistry
Это протокол PreviewRegistry, и мы можем просто посмотреть его документацию:
/// Registry protocol used to locate previews at runtime. Types conforming to this protocol are /// generated for you by the expansion of the `#Preview` macros. /// /// - Note: Previews should always be created using the `#Preview` macro syntax. /// Behavior for preview registries defined directly is undefined. @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) public protocol PreviewRegistry { static var fileID: String { get } static var line: Int { get } static var column: Int { get } static var preview: Preview { get } }
Из нее мы узнаем, что этот протокол необходим для поиска превью во время выполнения. Назначение полей fileID, line и column достаточно сложно предположить. Возможно, они нужны для определения конкретного View для превью, если их несколько в одном файле.
Preview
Интересно, что в PreviewRegistry у нас есть static var preview: Preview и в частности тип Preview. Информации о нём достаточно мало:
/// Base type for creating previews. /// /// Extensions in SwiftUI, UIKit, AppKit, and WidgetKit provide /// subject-matter-specific initializers. @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) public struct Preview { }
Так как понять, что из себя представляет эта структура, мы пока не можем, то попробуем извлечь немного больше информации, используя инструмент для просмотра деталей объекта Mirror:
// Получение наименования переменных внутри стркутуры `Preview` Mirror(reflecting: previewRegistry.preview).children.map(\.label)
Сопоставив данные из Mirror, я смог получить следующую структуру:
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) public struct Preview { var displayName: String? var source: ViewPreviewSource var fileID: String var line: Int var traits: [PreviewTrait] }
Теперь с этим можно работать. Давайте рассмортим части Preview подробнее, и начнем с ViewPreviewSource.
ViewPreviewSource
Еще из интересного: в Preview есть переменная source: ViewPreviewSource, которая на самом деле является просто обёрткой для нашего представления. ViewPreviewSource используется как контейнер, в котором может лежать как SwiftUI View, так и UIKit UIView или UIViewController. Благодаря этому механизму достигается поддержка работы Preview с UIKit, чего раньше превью не поддерживали без использования дополнительных обёрток. Таким образом, приложение может эксплуатировать возможности обоих фреймворков и позволяет без проблем создавать превью интерфейсов без необходимости вводить дополнительные абстракции или преобразования. Снова обратившись к Mirror, получаем примерный вид ViewPreviewSource структуры:
struct ViewPreviewSource<T> { var makeView: () -> T }
Зачем вообще углубляться?
Понимание того, как Apple использует свои же нововведения, очень сильно помогает в работе над собственными решениями. Например, этот разбор я проводил для проекта:
Он берёт реализацию превью от Apple и генерирует на их основе snapshot-тесты и демо приложение. Подробнее о Prefire можно узнать из доклада:
Что с этим можно сделать?
Исходя из того что я нашёл выше, можно использовать генерируемый макросом код, а именно протокол PreviewRegistry, для поиска превью во всём приложении — и при нахождении превью, которое реализует протокол, генерировать тесты.
Например, с помощью Sourcery мы можем найти все PreviewRegistry и получить нужные нам превью. Для Sourcery нужно будет создать специальный Stencil-шаблон (как это делать, я рассказал в видео выше). Вот так это может выглядеть (псевдокод):
extension Preview { /// Получение `View`, из `Preview` func makeView() -> AnyView { // Получение `ViewPreviewSource` c помощью `Mirror` let source = Mirror(reflecting: self).children.first(where: { $0.label == "source" })?.value // Получение `View` c помощью `Mirror` let viewBuilder = source.flatMap { Mirror(reflecting: $0).children.first?.value as? () -> (any View) } return viewBuilder.flatMap(AnyView.init) } } // Массив всех `Preview`, заполненный с помощью Sourcery var allPreviews: [Preview] = [] for preview in allPreviews { // Вызов библиотеки для snapshot- тестов snapshotTestsing(preview.makeView()) }
Что в итоге?
Таким образом, мы узнали немного больше о том, что происходит при работе макроса #Preview.
Можно сделать вывод о том, что макросы существенно упрощают жизнь разработчика и Apple уже предлагает нам свои решения на основе макросов.
В результате вместо громоздкого решения мы получаем более компактное и простое:
Старое решение:
struct Text_Preview: PreviewProvider { static var previews: some View { Text("Simple text") } }
Новое решение:
#Preview { Text("Simple text") }
ссылка на оригинал статьи https://habr.com/ru/companies/ozontech/articles/744216/
Добавить комментарий