Изнутри: Swift макрос — #Preview

от автора

Макрос #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 } }

Из нее мы узнаем, что этот протокол необходим для поиска превью во время выполнения. Назначение полей fileIDline и 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *